diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php index 259dd33f0..255ea87ac 100644 --- a/application/controllers/HostController.php +++ b/application/controllers/HostController.php @@ -8,26 +8,38 @@ use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Command\Object\GetObjectCommand; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; +use Icinga\Module\Icingadb\Common\Backend; 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\DependencyEdge; +use Icinga\Module\Icingadb\Model\DependencyNode; 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\Control\SearchBar\ObjectSuggestions; +use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; 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\DependencyNodeList; use Icinga\Module\Icingadb\Widget\ItemList\HostList; use Icinga\Module\Icingadb\Widget\ItemList\HistoryList; use Icinga\Module\Icingadb\Widget\ItemList\ServiceList; +use ipl\Orm\Query; +use ipl\Sql\Expression; +use ipl\Sql\Filter\Exists; use ipl\Stdlib\Filter; +use ipl\Web\Control\LimitControl; +use ipl\Web\Control\SortControl; use ipl\Web\Url; use ipl\Web\Widget\Tabs; +use Generator; class HostController extends Controller { @@ -37,9 +49,9 @@ class HostController extends Controller /** @var Host The host object */ protected $host; - public function init() + public function init(): void { - $name = $this->params->getRequired('name'); + $name = $this->params->shiftRequired('name'); $query = Host::on($this->getDb())->with(['state', 'icon_image', 'timeperiod']); $query @@ -51,17 +63,22 @@ public function init() /** @var Host $host */ $host = $query->first(); if ($host === null) { - throw new NotFoundError(t('Host not found')); + throw new NotFoundError($this->translate('Host not found')); } $this->host = $host; $this->loadTabsForObject($host); + $this->addControl((new HostList([$host])) + ->setViewMode('objectHeader') + ->setDetailActionsDisabled() + ->setNoSubjectLink()); + $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle($host->display_name); } - public function indexAction() + public function indexAction(): void { $serviceSummary = ServicestateSummary::on($this->getDb()); $serviceSummary->filter(Filter::equal('service.host_id', $this->host->id)); @@ -72,10 +89,6 @@ public function indexAction() $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)); @@ -84,7 +97,7 @@ public function indexAction() $this->setAutorefreshInterval(10); } - public function sourceAction() + public function sourceAction(): void { $this->assertPermission('icingadb/object/show-source'); @@ -97,17 +110,13 @@ public function sourceAction() $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() + public function historyAction(): Generator { $compact = $this->view->compact; // TODO: Find a less-legacy way.. @@ -141,7 +150,7 @@ public function historyAction() $sortControl = $this->createSortControl( $history, [ - 'history.event_time desc, history.event_type desc' => t('Event Time') + 'history.event_time desc, history.event_type desc' => $this->translate('Event Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); @@ -158,10 +167,6 @@ public function historyAction() yield $this->export($history); - $this->addControl((new HostList([$this->host])) - ->setViewMode('objectHeader') - ->setDetailActionsDisabled() - ->setNoSubjectLink()); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); @@ -182,7 +187,7 @@ public function historyAction() } } - public function servicesAction() + public function servicesAction(): Generator { if ($this->host->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); @@ -209,10 +214,10 @@ public function servicesAction() $sortControl = $this->createSortControl( $services, [ - 'service.display_name' => t('Name'), - 'service.state.severity desc,service.state.last_state_change desc' => t('Severity'), - 'service.state.soft_state' => t('Current State'), - 'service.state.last_state_change desc' => t('Last State Change') + 'service.display_name' => $this->translate('Name'), + 'service.state.severity desc,service.state.last_state_change desc' => $this->translate('Severity'), + 'service.state.soft_state' => $this->translate('Current State'), + 'service.state.last_state_change desc' => $this->translate('Last State Change') ] ); @@ -221,10 +226,6 @@ public function servicesAction() $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); @@ -235,25 +236,252 @@ public function servicesAction() $this->setAutorefreshInterval(10); } + public function parentsAction(): Generator + { + $nodesQuery = $this->fetchNodes(true); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + + $defaultSort = ['severity DESC', 'last_state_change DESC']; + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') + ], + $defaultSort + ); + + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar( + $nodesQuery, + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'name' + ] + ); + + if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { + if ($searchBar->hasBeenSubmitted()) { + $filter = $this->getFilter(); + } else { + $this->addControl($searchBar); + $this->sendMultipartUpdate(); + + return; + } + } else { + $filter = $searchBar->getFilter(); + } + + $nodesQuery->filter($filter); + + yield $this->export($nodesQuery); + + $this->addControl($paginationControl); + $this->addControl($sortControl); + $this->addControl($limitControl); + $this->addControl($viewModeSwitcher); + $this->addControl($searchBar); + + $this->addContent( + (new DependencyNodeList($nodesQuery)) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function childrenAction(): Generator + { + $nodesQuery = $this->fetchNodes(); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + + $defaultSort = ['severity DESC', 'last_state_change DESC']; + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') + ], + $defaultSort + ); + + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar( + $nodesQuery, + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'name' + ] + ); + + $searchBar->getSuggestionUrl()->setParam('isChildrenTab'); + $searchBar->getEditorUrl()->setParam('isChildrenTab'); + + if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { + if ($searchBar->hasBeenSubmitted()) { + $filter = $this->getFilter(); + } else { + $this->addControl($searchBar); + $this->sendMultipartUpdate(); + + return; + } + } else { + $filter = $searchBar->getFilter(); + } + + $nodesQuery->filter($filter); + + yield $this->export($nodesQuery); + + $this->addControl($paginationControl); + $this->addControl($sortControl); + $this->addControl($limitControl); + $this->addControl($viewModeSwitcher); + $this->addControl($searchBar); + + $this->addContent( + (new DependencyNodeList($nodesQuery)) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function completeAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $relation = $isChildrenTab ? 'parent' : 'child'; + + $suggestions = (new ObjectSuggestions()) + ->setModel(DependencyNode::class) + ->setBaseFilter(Filter::equal("$relation.host.id", $this->host->id)) + ->forRequest($this->getServerRequest()); + + $this->getDocument()->add($suggestions); + } + + public function searchEditorAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $redirectUrl = $isChildrenTab + ? Url::fromPath('icingadb/host/children', ['name' => $this->host->name]) + : Url::fromPath('icingadb/host/parents', ['name' => $this->host->name]); + + $editor = $this->createSearchEditor( + DependencyNode::on($this->getDb()), + $redirectUrl, + [ + LimitControl::DEFAULT_LIMIT_PARAM, + SortControl::DEFAULT_SORT_PARAM, + ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, + 'name' + ] + ); + + if ($isChildrenTab) { + $editor->getSuggestionUrl()->setParam('isChildrenTab'); + } + + $this->getDocument()->add($editor); + $this->setTitle($this->translate('Adjust Filter')); + } + + /** + * Fetch the nodes for the current host + * + * @param bool $fetchParents Whether to fetch the parents or the children + * + * @return Query + */ + protected function fetchNodes(bool $fetchParents = false): Query + { + $query = DependencyNode::on($this->getDb()) + ->with([ + 'host', + 'host.state', + 'host.state.last_comment', + 'service', + 'service.state', + 'service.state.last_comment', + 'service.host', + 'service.host.state', + 'redundancy_group', + 'redundancy_group.state' + ]) + ->setResultSetClass(VolatileStateResults::class); + + $this->joinFix($query, $this->host->id, $fetchParents); + + $this->applyRestrictions($query); + + return $query; + } + protected function createTabs(): Tabs { + if (Backend::getDbSchemaVersion() < 6) { + $hasDependencyNode = false; + } else { + $hasDependencyNode = DependencyNode::on($this->getDb()) + ->columns([new Expression('1')]) + ->filter(Filter::all( + Filter::equal('host_id', $this->host->id), + Filter::unlike('service_id', '*') + )) + ->first() !== null; + } + $tabs = $this->getTabs() ->add('index', [ - 'label' => t('Host'), + 'label' => $this->translate('Host'), 'url' => Links::host($this->host) ]) ->add('services', [ - 'label' => t('Services'), + 'label' => $this->translate('Services'), 'url' => HostLinks::services($this->host) ]) ->add('history', [ - 'label' => t('History'), - 'url' => HostLinks::history($this->host) + 'label' => $this->translate('History'), + 'url' => HostLinks::history($this->host) + ]); + + if ($hasDependencyNode) { + $tabs->add('parents', [ + 'label' => $this->translate('Parents'), + 'url' => Url::fromPath('icingadb/host/parents', ['name' => $this->host->name]) + ])->add('children', [ + 'label' => $this->translate('Children'), + 'url' => Url::fromPath('icingadb/host/children', ['name' => $this->host->name]) ]); + } if ($this->hasPermission('icingadb/object/show-source')) { $tabs->add('source', [ - 'label' => t('Source'), + 'label' => $this->translate('Source'), 'url' => Links::hostSource($this->host) ]); } @@ -265,14 +493,12 @@ protected function createTabs(): Tabs return $tabs; } - protected function setTitleTab(string $name) + protected function setTitleTab(string $name): void { $tab = $this->createTabs()->get($name); if ($tab !== null) { - $tab->setActive(); - - $this->setTitle($tab->getLabel()); + $this->getTabs()->activate($name); } } @@ -290,4 +516,42 @@ protected function getDefaultTabControls(): array { return [(new HostList([$this->host]))->setDetailActionsDisabled()->setNoSubjectLink()]; } + + /** + * Filter the query to only include (direct) parents or children of the given object. + * + * @todo This is a workaround, remove it once https://github.com/Icinga/ipl-orm/issues/76 is fixed + * + * @param Query $query + * @param string $objectId + * @param bool $fetchParents Fetch parents if true, children otherwise + */ + protected function joinFix(Query $query, string $objectId, bool $fetchParents = false): void + { + $filterTable = $fetchParents ? 'child' : 'parent'; + $utilizeType = $fetchParents ? 'parent' : 'child'; + + $edge = DependencyEdge::on($this->getDb()) + ->utilize($utilizeType) + ->columns([new Expression('1')]) + ->filter(Filter::equal("$filterTable.host.id", $objectId)) + ->filter(Filter::unlike("$filterTable.service.id", '*')); + + $edge->getFilter()->metaData()->set('forceOptimization', false); + + $resolver = $edge->getResolver(); + + $edgeAlias = $resolver->getAlias( + $resolver->resolveRelation($resolver->qualifyPath($utilizeType, $edge->getModel()->getTableName())) + ->getTarget() + ); + + $query->filter(new Exists( + $edge->assembleSelect() + ->where( + "$edgeAlias.id = " + . $query->getResolver()->qualifyColumn('id', $query->getModel()->getTableName()) + ) + )); + } } diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index 25f56b729..835562e2c 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -13,18 +13,28 @@ use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Hook\TabHook\HookActions; +use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Redis\VolatileStateResults; +use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; +use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; 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\DependencyNodeList; use Icinga\Module\Icingadb\Widget\ItemList\HistoryList; use Icinga\Module\Icingadb\Widget\ItemList\ServiceList; +use ipl\Orm\Query; +use ipl\Sql\Expression; use ipl\Stdlib\Filter; +use ipl\Web\Control\LimitControl; +use ipl\Web\Control\SortControl; use ipl\Web\Url; +use ipl\Web\Widget\Tabs; +use Generator; class ServiceController extends Controller { @@ -34,10 +44,10 @@ class ServiceController extends Controller /** @var Service The service object */ protected $service; - public function init() + public function init(): void { - $name = $this->params->getRequired('name'); - $hostName = $this->params->getRequired('host.name'); + $name = $this->params->shiftRequired('name'); + $hostName = $this->params->shiftRequired('host.name'); $query = Service::on($this->getDb()) ->with([ @@ -63,30 +73,31 @@ public function init() /** @var Service $service */ $service = $query->first(); if ($service === null) { - throw new NotFoundError(t('Service not found')); + throw new NotFoundError($this->translate('Service not found')); } $this->service = $service; $this->loadTabsForObject($service); + $this->addControl((new ServiceList([$service])) + ->setViewMode('objectHeader') + ->setDetailActionsDisabled() + ->setNoSubjectLink()); + $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle( - t('%s on %s', ' on '), + $this->translate('%s on %s', ' on '), $service->display_name, $service->host->display_name ); } - public function indexAction() + public function indexAction(): void { 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)); @@ -95,7 +106,144 @@ public function indexAction() $this->setAutorefreshInterval(10); } - public function sourceAction() + public function parentsAction(): Generator + { + $nodesQuery = $this->fetchNodes(true); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + + $defaultSort = ['severity DESC', 'last_state_change DESC']; + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') + ], + $defaultSort + ); + + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar( + $nodesQuery, + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'name', + 'host.name' + ] + ); + + if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { + if ($searchBar->hasBeenSubmitted()) { + $filter = $this->getFilter(); + } else { + $this->addControl($searchBar); + $this->sendMultipartUpdate(); + + return; + } + } else { + $filter = $searchBar->getFilter(); + } + + $nodesQuery->filter($filter); + + yield $this->export($nodesQuery); + + $this->addControl($paginationControl); + $this->addControl($sortControl); + $this->addControl($limitControl); + $this->addControl($viewModeSwitcher); + $this->addControl($searchBar); + + $this->addContent( + (new DependencyNodeList($nodesQuery)) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function childrenAction(): Generator + { + $nodesQuery = $this->fetchNodes(); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + + $defaultSort = ['severity DESC', 'last_state_change DESC']; + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') + ], + $defaultSort + ); + + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar( + $nodesQuery, + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'name', + 'host.name' + ] + ); + + $searchBar->getSuggestionUrl()->setParam('isChildrenTab'); + $searchBar->getEditorUrl()->setParam('isChildrenTab'); + + if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { + if ($searchBar->hasBeenSubmitted()) { + $filter = $this->getFilter(); + } else { + $this->addControl($searchBar); + $this->sendMultipartUpdate(); + + return; + } + } else { + $filter = $searchBar->getFilter(); + } + + $nodesQuery->filter($filter); + + yield $this->export($nodesQuery); + + $this->addControl($paginationControl); + $this->addControl($sortControl); + $this->addControl($limitControl); + $this->addControl($viewModeSwitcher); + $this->addControl($searchBar); + + $this->addContent( + (new DependencyNodeList($nodesQuery)) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function sourceAction(): void { $this->assertPermission('icingadb/object/show-source'); @@ -108,17 +256,13 @@ public function sourceAction() $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() + public function historyAction(): Generator { $compact = $this->view->compact; // TODO: Find a less-legacy way.. @@ -153,7 +297,7 @@ public function historyAction() $sortControl = $this->createSortControl( $history, [ - 'history.event_time desc, history.event_type desc' => t('Event Time') + 'history.event_time desc, history.event_type desc' => $this->translate('Event Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); @@ -170,10 +314,6 @@ public function historyAction() yield $this->export($history); - $this->addControl((new ServiceList([$this->service])) - ->setViewMode('objectHeader') - ->setDetailActionsDisabled() - ->setNoSubjectLink()); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); @@ -194,21 +334,128 @@ public function historyAction() } } - protected function createTabs() + public function completeAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $relation = $isChildrenTab ? 'parent' : 'child'; + + $suggestions = (new ObjectSuggestions()) + ->setModel(DependencyNode::class) + ->setBaseFilter(Filter::equal("$relation.service.id", $this->service->id)) + ->forRequest($this->getServerRequest()); + + $this->getDocument()->add($suggestions); + } + + public function searchEditorAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $redirectUrl = $isChildrenTab + ? Url::fromPath( + 'icingadb/service/children', + ['name' => $this->service->name, 'host.name' => $this->service->host->name] + ) + : Url::fromPath( + 'icingadb/service/parents', + ['name' => $this->service->name, 'host.name' => $this->service->host->name] + ); + + $editor = $this->createSearchEditor( + DependencyNode::on($this->getDb()), + $redirectUrl, + [ + LimitControl::DEFAULT_LIMIT_PARAM, + SortControl::DEFAULT_SORT_PARAM, + ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, + 'name', + 'host.name' + ] + ); + + if ($isChildrenTab) { + $editor->getSuggestionUrl()->setParam('isChildrenTab'); + } + + $this->getDocument()->add($editor); + $this->setTitle($this->translate('Adjust Filter')); + } + + /** + * Fetch the nodes for the current service + * + * @param bool $fetchParents Whether to fetch the parents or the children + * + * @return Query + */ + protected function fetchNodes(bool $fetchParents = false): Query + { + $query = DependencyNode::on($this->getDb()) + ->with([ + 'host', + 'host.state', + 'host.state.last_comment', + 'service', + 'service.state', + 'service.state.last_comment', + 'service.host', + 'service.host.state', + 'redundancy_group', + 'redundancy_group.state' + ]) + ->filter(Filter::equal( + sprintf('%s.service.id', $fetchParents ? 'child' : 'parent'), + $this->service->id + )) + ->setResultSetClass(VolatileStateResults::class); + + $this->applyRestrictions($query); + + return $query; + } + + protected function createTabs(): Tabs { + if (Backend::getDbSchemaVersion() < 6) { + $hasDependencyNode = false; + } else { + $hasDependencyNode = DependencyNode::on($this->getDb()) + ->columns([new Expression('1')]) + ->filter(Filter::all( + Filter::equal('service_id', $this->service->id), + Filter::equal('host_id', $this->service->host_id) + )) + ->first() !== null; + } + $tabs = $this->getTabs() ->add('index', [ - 'label' => t('Service'), + 'label' => $this->translate('Service'), 'url' => Links::service($this->service, $this->service->host) ]) ->add('history', [ - 'label' => t('History'), + 'label' => $this->translate('History'), 'url' => ServiceLinks::history($this->service, $this->service->host) ]); + if ($hasDependencyNode) { + $tabs->add('parents', [ + 'label' => $this->translate('Parents'), + 'url' => Url::fromPath( + 'icingadb/service/parents', + ['name' => $this->service->name, 'host.name' => $this->service->host->name] + ) + ])->add('children', [ + 'label' => $this->translate('Children'), + 'url' => Url::fromPath( + 'icingadb/service/children', + ['name' => $this->service->name, 'host.name' => $this->service->host->name] + ) + ]); + } + if ($this->hasPermission('icingadb/object/show-source')) { $tabs->add('source', [ - 'label' => t('Source'), + 'label' => $this->translate('Source'), 'url' => Links::serviceSource($this->service, $this->service->host) ]); } @@ -223,14 +470,12 @@ protected function createTabs() return $tabs; } - protected function setTitleTab(string $name) + protected function setTitleTab(string $name): void { $tab = $this->createTabs()->get($name); if ($tab !== null) { - $tab->setActive(); - - $this->setTitle($tab->getLabel()); + $this->getTabs()->activate($name); } } diff --git a/library/Icingadb/Data/CsvResultSetUtils.php b/library/Icingadb/Data/CsvResultSetUtils.php index 61995d3a2..862260cc8 100644 --- a/library/Icingadb/Data/CsvResultSetUtils.php +++ b/library/Icingadb/Data/CsvResultSetUtils.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeZone; +use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Orm\Model; @@ -67,7 +68,8 @@ protected function extractKeysAndValues(Model $model, string $path = ''): array public static function stream(Query $query): void { - if ($query->getModel() instanceof Host || $query->getModel() instanceof Service) { + $model = $query->getModel(); + if ($model instanceof Host || $model instanceof Service || $model instanceof DependencyNode) { $query->setResultSetClass(VolatileCsvResults::class); } else { $query->setResultSetClass(__CLASS__); diff --git a/library/Icingadb/Data/JsonResultSetUtils.php b/library/Icingadb/Data/JsonResultSetUtils.php index 8b8857122..dc78fe094 100644 --- a/library/Icingadb/Data/JsonResultSetUtils.php +++ b/library/Icingadb/Data/JsonResultSetUtils.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeZone; +use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Util\Json; @@ -61,7 +62,8 @@ protected function createObject(Model $model): array public static function stream(Query $query): void { - if ($query->getModel() instanceof Host || $query->getModel() instanceof Service) { + $model = $query->getModel(); + if ($model instanceof Host || $model instanceof Service || $model instanceof DependencyNode) { $query->setResultSetClass(VolatileJsonResults::class); } else { $query->setResultSetClass(__CLASS__); diff --git a/library/Icingadb/Widget/ItemList/DependencyNodeList.php b/library/Icingadb/Widget/ItemList/DependencyNodeList.php index c95f883fa..3457b16e9 100644 --- a/library/Icingadb/Widget/ItemList/DependencyNodeList.php +++ b/library/Icingadb/Widget/ItemList/DependencyNodeList.php @@ -28,14 +28,23 @@ protected function getItemClass(): string protected function createListItem(object $data): BaseListItem { + $viewMode = $this->getViewMode(); /** @var UnreachableParent|DependencyNode $data */ if ($data->redundancy_group_id !== null) { + if ($viewMode === 'minimal') { + return new RedundancyGroupListItemMinimal($data->redundancy_group, $this); + } + + if ($viewMode === 'detailed') { + $this->removeAttribute('class', 'default-layout'); + } + return new RedundancyGroupListItem($data->redundancy_group, $this); } $object = $data->service_id !== null ? $data->service : $data->host; - switch ($this->getViewMode()) { + switch ($viewMode) { case 'minimal': $class = $object instanceof Host ? HostListItemMinimal::class : ServiceListItemMinimal::class; break; diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItemMinimal.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItemMinimal.php new file mode 100644 index 000000000..be6b96cb6 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItemMinimal.php @@ -0,0 +1,18 @@ + * { + margin: 0 .28125em; // 0 calculated   width + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} + .redundancy-group-list-item { - .caption { - display: flex; - justify-content: end; + .caption .object-statistics { + justify-self: end; + } +} + +.minimal > .redundancy-group-list-item { + .caption .object-statistics { + font-size: 0.75em; } }