From e426edde859e50ee27df88ed6b3eacd0b3ae43c6 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Wed, 16 Oct 2024 16:27:57 +0200 Subject: [PATCH 01/12] Models: Add missing columns and methods RedundancyGroupSummary: Add columns for `(un)acknowledged` count --- library/Icingadb/Model/DependencyNode.php | 9 +++++++ library/Icingadb/Model/RedundancyGroup.php | 7 +++++ .../Icingadb/Model/RedundancyGroupSummary.php | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/library/Icingadb/Model/DependencyNode.php b/library/Icingadb/Model/DependencyNode.php index c30677ad3..635d3a834 100644 --- a/library/Icingadb/Model/DependencyNode.php +++ b/library/Icingadb/Model/DependencyNode.php @@ -47,6 +47,15 @@ public function getColumns(): array ]; } + public function getSearchColumns(): array + { + return [ + 'host.name_ci', + 'service.name_ci', + 'redundancy_group.display_name' + ]; + } + public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ diff --git a/library/Icingadb/Model/RedundancyGroup.php b/library/Icingadb/Model/RedundancyGroup.php index 2965f0634..efe44d9b2 100644 --- a/library/Icingadb/Model/RedundancyGroup.php +++ b/library/Icingadb/Model/RedundancyGroup.php @@ -44,6 +44,13 @@ public function getColumns(): array ]; } + public function getColumnDefinitions(): array + { + return [ + 'display_name' => t('Redundancy Group Display Name') + ]; + } + public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ diff --git a/library/Icingadb/Model/RedundancyGroupSummary.php b/library/Icingadb/Model/RedundancyGroupSummary.php index 35ad03aaf..71a790d17 100644 --- a/library/Icingadb/Model/RedundancyGroupSummary.php +++ b/library/Icingadb/Model/RedundancyGroupSummary.php @@ -131,6 +131,32 @@ public function getSummaryColumns(): array 'from.to.service.state.is_handled', 'from.to.service.state.is_reachable' ] + ), + 'nodes_acknowledged' => new Expression( + 'SUM(CASE' + . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 'y' THEN 1 ELSE 0 END)" + . " WHEN %s = 'y' THEN 1" + . ' ELSE 0' + . ' END)', + [ + 'from.to.service_id', + 'from.to.service.state.is_acknowledged', + 'from.to.host.state.is_acknowledged', + ] + ), + 'nodes_problems_unacknowledged' => new Expression( + 'SUM(CASE' + . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 'y' AND %s = 'n' THEN 1 ELSE 0 END)" + . " WHEN %s = 'y' AND %s = 'n' THEN 1" + . ' ELSE 0' + . ' END)', + [ + 'from.to.service_id', + 'from.to.service.state.is_problem', + 'from.to.service.state.is_acknowledged', + 'from.to.host.state.is_problem', + 'from.to.host.state.is_acknowledged', + ] ) ]; } From db77a90827dc06710d7ff6791952554f0b3f60ed Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 12 Nov 2024 09:39:36 +0100 Subject: [PATCH 02/12] Add RedundancyGroupDetailExtentionHook --- .../ObjectDetailExtensionHook.php | 4 ++++ .../RedundancyGroupDetailExtensionHook.php | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php diff --git a/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php b/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php index 12b52fc39..08ac36762 100644 --- a/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php +++ b/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php @@ -14,6 +14,7 @@ use Icinga\Module\Icingadb\Hook\UsergroupDetailExtensionHook; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; @@ -48,6 +49,9 @@ final public static function loadExtensions(Model $object): array case $object instanceof Service: $hookName = 'Icingadb\\ServiceDetailExtension'; break; + case $object instanceof RedundancyGroup: + $hookName = 'Icingadb\\RedundancyGroupDetailExtension'; + break; case $object instanceof User: $hookName = 'Icingadb\\UserDetailExtension'; break; diff --git a/library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php b/library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php new file mode 100644 index 000000000..7cf417358 --- /dev/null +++ b/library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php @@ -0,0 +1,21 @@ + Date: Tue, 12 Nov 2024 09:43:43 +0100 Subject: [PATCH 03/12] Introduce class `RedundancyGroupDetail` rg-detail --- library/Icingadb/Model/UnreachableParent.php | 4 +- .../Widget/Detail/RedundancyGroupDetail.php | 146 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 library/Icingadb/Widget/Detail/RedundancyGroupDetail.php diff --git a/library/Icingadb/Model/UnreachableParent.php b/library/Icingadb/Model/UnreachableParent.php index 647e989a7..6ba633aac 100644 --- a/library/Icingadb/Model/UnreachableParent.php +++ b/library/Icingadb/Model/UnreachableParent.php @@ -126,8 +126,10 @@ private static function selectNodes(Connection $db, Model $root): Select Filter::equal('host_id', $root->host_id), Filter::equal('service_id', $root->id) )); + } elseif ($root instanceof RedundancyGroup) { + $rootQuery->filter(Filter::all(Filter::equal('redundancy_group_id', $root->id))); } else { - throw new InvalidArgumentException('Root node must be either a host or a service'); + throw new InvalidArgumentException('Root node must be either a host, service or a redundancy group'); } $nodeQuery = DependencyEdge::on($db) diff --git a/library/Icingadb/Widget/Detail/RedundancyGroupDetail.php b/library/Icingadb/Widget/Detail/RedundancyGroupDetail.php new file mode 100644 index 000000000..1c07e4c13 --- /dev/null +++ b/library/Icingadb/Widget/Detail/RedundancyGroupDetail.php @@ -0,0 +1,146 @@ + ['redundancygroup-detail'], + 'data-pdfexport-page-breaks-at' => 'h2' + ]; + + protected $tag = 'div'; + + /** + * Create a new redundancy group detail widget + * + * @param RedundancyGroup $group + */ + public function __construct(RedundancyGroup $group) + { + $this->group = $group; + } + + /** + * Create hook extensions + * + * @return array + */ + protected function createExtensions(): array + { + return ObjectDetailExtensionHook::loadExtensions($this->group); + } + + /** + * Create a list of root problems if the redundancy group fails + * + * @return ?BaseHtmlElement[] + */ + protected function createRootProblems(): ?array + { + if (! $this->group->state->failed) { + return null; + } + + $rootProblems = UnreachableParent::on($this->getDb(), $this->group) + ->with([ + 'redundancy_group', + 'redundancy_group.state', + 'host', + 'host.state', + 'host.icon_image', + 'host.state.last_comment', + 'service', + 'service.state', + 'service.icon_image', + 'service.state.last_comment', + 'service.host', + 'service.host.state', + ]) + ->setResultSetClass(VolatileStateResults::class) + ->orderBy([ + 'host.state.severity', + 'host.state.last_state_change', + 'service.state.severity', + 'service.state.last_state_change', + 'redundancy_group.state.failed', + 'redundancy_group.state.last_state_change' + ], SORT_DESC); + + $this->applyRestrictions($rootProblems); + + return [ + HtmlElement::create('h2', null, Text::create($this->translate('Root Problems'))), + (new DependencyNodeList($rootProblems))->setEmptyStateMessage( + $this->translate('You are not authorized to view these objects.') + ) + ]; + } + + /** + * Create a list of group members + * + * @return BaseHtmlElement[] + */ + protected function createGroupMembers(): array + { + $membersQuery = DependencyNode::on($this->getDb()) + ->with([ + 'host', + 'host.state', + 'service', + 'service.state', + 'service.host', + 'service.host.state' + ]) + ->filter(Filter::equal('child.redundancy_group.id', $this->group->id)) + ->limit(5) + ->peekAhead(); + + $this->applyRestrictions($membersQuery); + + // TODO: Do not execute at this time. The widget may be replaced by a hook in which case the result is unused. + $members = $membersQuery->execute(); + + return [ + HtmlElement::create('h2', null, Text::create($this->translate('Group Members'))), + (new DependencyNodeList($members)) + ->setEmptyStateMessage($this->translate('You are not authorized to view these objects.')), + (new ShowMore($members, Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->group->id]))) + ->setBaseTarget('_self') + ]; + } + + protected function assemble(): void + { + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 0 => $this->createRootProblems(), + 510 => $this->createGroupMembers(), + ], $this->createExtensions())); + } +} From eee916f7357eadd1f5504b95bd862e975e460d37 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 12 Nov 2024 19:26:31 +0100 Subject: [PATCH 04/12] Introduce RedundancygroupController --- .../controllers/RedundancygroupController.php | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 application/controllers/RedundancygroupController.php diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php new file mode 100644 index 000000000..e2ce406d7 --- /dev/null +++ b/application/controllers/RedundancygroupController.php @@ -0,0 +1,285 @@ +groupId = $this->params->shiftRequired('id'); + + $query = RedundancyGroup::on($this->getDb()) + ->with(['state']) + ->filter(Filter::equal('id', $this->groupId)); + + $this->applyRestrictions($query); + + $this->group = $query->first(); + + if ($this->group === null) { + throw new NotFoundError(t('Redundancy Group not found')); + } + + $this->setTitleTab($this->getRequest()->getActionName()); + $this->setTitle($this->group->display_name); + + $this->addControl(new HtmlElement('div', null, Text::create($this->group->display_name))); + $this->addFooter( + new DependencyNodeStatistics( + RedundancyGroupSummary::on($this->getDb()) + ->filter(Filter::equal('id', $this->groupId)) + ->first() + ) + ); + } + + public function indexAction(): void + { + $this->addContent(new RedundancyGroupDetail($this->group)); + } + + public function membersAction(): \Generator + { + $nodesQuery = $this->fetchNodes(true); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'name' => t('Name'), + 'severity desc, last_state_change desc' => t('Severity'), + 'state' => t('Current State'), + 'last_state_change desc' => t('Last State Change') + ] + ); + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar($nodesQuery, + Links::redundancyGroupMembers($this->group), + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'id' + ] + ); + + 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->execute())) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function childrenAction() + { + $nodesQuery = $this->fetchNodes(); + + $limitControl = $this->createLimitControl(); + $paginationControl = $this->createPaginationControl($nodesQuery); + $sortControl = $this->createSortControl( + $nodesQuery, + [ + 'name' => t('Name'), + 'severity desc, last_state_change desc' => t('Severity'), + 'state' => t('Current State'), + 'last_state_change desc' => t('Last State Change') + ] + ); + $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); + + $searchBar = $this->createSearchBar( + $nodesQuery, + Links::redundancyGroupChildren($this->group), + [ + $limitControl->getLimitParam(), + $sortControl->getSortParam(), + $viewModeSwitcher->getViewModeParam(), + 'id' + ] + ); + + $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->execute())) + ->setViewMode($viewModeSwitcher->getViewMode()) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + + $this->setAutorefreshInterval(10); + } + + public function completeAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $column = $isChildrenTab ? 'parent' : 'child'; + + $suggestions = (new ObjectSuggestions()) + ->setModel(DependencyNode::class) + ->setBaseFilter(Filter::equal("$column.redundancy_group.id", $this->groupId)) + ->forRequest($this->getServerRequest()); + + $this->getDocument()->add($suggestions); + } + + public function searchEditorAction(): void + { + $isChildrenTab = $this->params->shift('isChildrenTab'); + $redirectUrl = $isChildrenTab + ? Links::redundancyGroupChildren($this->group) + : Links::redundancyGroupMembers($this->group); + + $editor = $this->createSearchEditor(DependencyNode::on($this->getDb()), + $redirectUrl, + [ + LimitControl::DEFAULT_LIMIT_PARAM, + SortControl::DEFAULT_SORT_PARAM, + ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, + 'id' + ] + ); + + if ($isChildrenTab) { + $editor->getSuggestionUrl()->setParam('isChildrenTab'); + } + + $this->getDocument()->add($editor); + $this->setTitle(t('Adjust Filter')); + } + + protected function createTabs(): Tabs + { + $tabs = $this->getTabs() + ->add('index', [ + 'label' => t('Redundancy Group'), + 'url' => Links::redundancyGroup($this->group) + ]) + ->add('members', [ + 'label' => t('Members'), + 'url' => Links::redundancyGroupMembers($this->group) + ]) + ->add('children', [ + 'label' => t('Children'), + 'url' => Links::redundancyGroupChildren($this->group) + ]); + + return $tabs; + } + + protected function setTitleTab(string $name): void + { + $tab = $this->createTabs()->get($name); + + if ($tab !== null) { + $this->getTabs()->activate($name); + } + } + + /** + * Fetch the nodes for the current group + * + * @param bool $fetchParents Whether to fetch the parents or the children + * + * @return Query + */ + private function fetchNodes(bool $fetchParents = false): Query + { + $filterColumn = sprintf( + '%s.redundancy_group.id', + $fetchParents ? 'child' : 'parent' + ); + + return DependencyNode::on($this->getDb()) + ->with([ + 'host', + 'host.state', + 'service', + 'service.state', + 'service.host', + 'service.host.state' + ]) + ->filter(Filter::equal($filterColumn, $this->groupId)); + } +} From fc65a18bb6b940200c9943f2b143d8288023f442 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 12 Nov 2024 17:53:17 +0100 Subject: [PATCH 05/12] Add MultiSelectQuickActions support --- .../controllers/RedundancygroupController.php | 61 +++++++++++- .../Authentication/ObjectAuthorization.php | 11 ++- library/Icingadb/Data/DependencyNodes.php | 80 +++++++++++++++ .../Widget/Detail/MultiselectQuickActions.php | 99 +++++++++++++++++-- 4 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 library/Icingadb/Data/DependencyNodes.php diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index e2ce406d7..f6ca1a2ec 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -5,7 +5,9 @@ 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\Data\DependencyNodes; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\RedundancyGroupSummary; @@ -13,6 +15,7 @@ use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics; +use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupDetail; use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList; use ipl\Html\HtmlElement; @@ -21,10 +24,13 @@ use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; +use ipl\Web\Url; use ipl\Web\Widget\Tabs; class RedundancygroupController extends Controller { + use CommandActions; + /** @var string */ protected $groupId; @@ -33,7 +39,13 @@ class RedundancygroupController extends Controller public function init(): void { - $this->groupId = $this->params->shiftRequired('id'); + // in case of quick actions, param id is not given + $groupId = $this->params->shift('child.redundancy_group.id'); + if ($groupId === null) { + $groupId = $this->params->shiftRequired('id'); + } + + $this->groupId = $groupId; $query = RedundancyGroup::on($this->getDb()) ->with(['state']) @@ -62,6 +74,20 @@ public function init(): void public function indexAction(): void { + $summary = RedundancyGroupSummary::on($this->getDb()) + ->filter(Filter::equal('id', $this->groupId)); + + $this->filter($summary); + + // The base filter is required to fetch the correct objects for MultiselectQuickActions::isGrantedOnType() check + $this->addControl( + (new MultiselectQuickActions('dependency_node', $summary->first())) + ->setBaseFilter(Filter::equal('child.redundancy_group.id', $this->groupId)) + ->setAllowToProcessCheckResults(false) + ->setColumnPrefix('nodes') + ->setUrlPath('icingadb/redundancygroup') + ); + $this->addContent(new RedundancyGroupDetail($this->group)); } @@ -282,4 +308,37 @@ private function fetchNodes(bool $fetchParents = false): Query ]) ->filter(Filter::equal($filterColumn, $this->groupId)); } + + protected function fetchCommandTargets() + { + $filter = Filter::all(Filter::equal('child.redundancy_group.id', $this->groupId)); + + if ($this->getRequest()->getActionName() === 'acknowledge') { + $filter->add( + Filter::any( + Filter::all( + Filter::unlike('child.service.id', '*'), + Filter::equal('host.state.is_problem', 'y'), + Filter::equal('host.state.is_acknowledged', 'n') + ), + Filter::all( + Filter::equal('service.state.is_problem', 'y'), + Filter::equal('service.state.is_acknowledged', 'n') + ) + ) + ); + } + + return new DependencyNodes($filter); + } + + protected function getCommandTargetsUrl(): Url + { + return Links::redundancyGroup($this->group); + } + + public function processCheckresultAction(): void + { + $this->httpBadRequest('Check result submission not implemented yet'); + } } diff --git a/library/Icingadb/Authentication/ObjectAuthorization.php b/library/Icingadb/Authentication/ObjectAuthorization.php index 988e8f01f..d49342cb8 100644 --- a/library/Icingadb/Authentication/ObjectAuthorization.php +++ b/library/Icingadb/Authentication/ObjectAuthorization.php @@ -6,6 +6,7 @@ use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use InvalidArgumentException; @@ -85,6 +86,9 @@ public static function grantsOnType(string $permission, string $type, Filter\Rul case 'service': $for = Service::class; break; + case 'dependency_node': + $for = DependencyNode::class; + break; default: throw new InvalidArgumentException(sprintf('Unknown type "%s"', $type)); } @@ -161,13 +165,16 @@ protected function loadGrants(string $model, Filter\Rule $filter, string $cacheK $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/objects')); } - if ($tableName === 'host' || $tableName === 'service') { + if ($tableName === 'host' || $tableName === 'service' || $tableName === 'dependency_node') { if (($restriction = $role->getRestrictions('icingadb/filter/hosts'))) { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/hosts')); } } - if ($tableName === 'service' && ($restriction = $role->getRestrictions('icingadb/filter/services'))) { + if ( + ($tableName === 'dependency_node' || $tableName === 'service') + && ($restriction = $role->getRestrictions('icingadb/filter/services')) + ) { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/services')); } diff --git a/library/Icingadb/Data/DependencyNodes.php b/library/Icingadb/Data/DependencyNodes.php new file mode 100644 index 000000000..92ced025d --- /dev/null +++ b/library/Icingadb/Data/DependencyNodes.php @@ -0,0 +1,80 @@ +filter = $filter; + } + + public function getIterator(): ArrayIterator + { + if ($this->nodes === null) { + $membersQuery = DependencyNode::on($this->getDb()) + ->with([ + 'host', + 'host.state', + 'service', + 'service.state', + 'service.host' + ]) + ->filter($this->filter); + + $this->applyRestrictions($membersQuery); + + $nodes = []; + foreach ($membersQuery as $node) { + $nodes[] = $node->service_id !== null ? $node->service : $node->host; + } + + $this->nodes = new ArrayIterator($nodes); + } + + return $this->nodes; + } + + public function getFilter(): Filter\Rule + { + return $this->filter; + } + + public function count(): int + { + return $this->getIterator()->count(); + } + + public function getModel() + { + return new Host(); + } +} diff --git a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php index b80ec9df2..b9a9f0c84 100644 --- a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php +++ b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php @@ -10,7 +10,6 @@ use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Stdlib\BaseFilter; -use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Icon; @@ -27,17 +26,100 @@ class MultiselectQuickActions extends BaseHtmlElement protected $defaultAttributes = ['class' => 'quick-actions']; + /** @var bool Whether to allow process check results */ + protected $allowToProcessCheckResults = true; + + /** @var ?string The summary column prefix */ + protected $columnPrefix; + + /** @var ?string The url path for {@see getLink()} method (default: `icingadb/$this->type . 's'`) */ + protected $urlPath; + public function __construct($type, $summary) { $this->summary = $summary; $this->type = $type; } + /** + * Set the summary column prefix + * + * @param string $columnPrefix + * + * @return $this + */ + public function setColumnPrefix(string $columnPrefix): self + { + $this->columnPrefix = $columnPrefix; + + return $this; + } + + /** + * Get the summary column prefix (default: `$this->type . 's'`) + * + * @return string + */ + public function getColumnPrefix(): string + { + if ($this->columnPrefix === null) { + $this->columnPrefix = $this->type . 's'; + } + + return $this->columnPrefix; + } + + /** + * Set the url path for {@see getLink()} method + * + * Omits the trailing slashes + * + * @param string $urlPath + * + * @return $this + */ + public function setUrlPath(string $urlPath): self + { + $this->urlPath = rtrim($urlPath, '/'); + + return $this; + } + + /** + * Get the url path for {@see getLink()} method + * + * If not set `icingadb/$this->type . 's'` is used + * + * @return string + */ + public function getUrlPath(): string + { + if ($this->urlPath === null) { + $this->urlPath = "icingadb/{$this->type}s"; + } + + return $this->urlPath; + } + + /** + * Set whether to allow process check results + * + * @param bool $state + * + * @return $this + */ + public function setAllowToProcessCheckResults(bool $state = true): self + { + $this->allowToProcessCheckResults = $state; + + return $this; + } + protected function assemble() { - $unacknowledged = "{$this->type}s_problems_unacknowledged"; - $acks = "{$this->type}s_acknowledged"; - $activeChecks = "{$this->type}s_active_checks_enabled"; + $unacknowledged = "{$this->getColumnPrefix()}_problems_unacknowledged"; + $acks = "{$this->getColumnPrefix()}_acknowledged"; + $activeChecks = "{$this->getColumnPrefix()}_active_checks_enabled"; if ( $this->summary->$unacknowledged > $this->summary->$acks @@ -76,7 +158,7 @@ protected function assemble() if ( $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) || ( - $this->summary->$activeChecks > 0 + ! empty($this->summary->$activeChecks) && $this->isGrantedOnType( 'icingadb/command/schedule-check/active-only', $this->type, @@ -132,7 +214,7 @@ protected function assemble() if ( $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) || ( - $this->summary->$activeChecks > 0 + ! empty($this->summary->$activeChecks) && $this->isGrantedOnType( 'icingadb/command/schedule-check/active-only', $this->type, @@ -150,7 +232,8 @@ protected function assemble() } if ( - $this->isGrantedOnType( + $this->allowToProcessCheckResults + && $this->isGrantedOnType( 'icingadb/command/process-check-result', $this->type, $this->getBaseFilter(), @@ -188,7 +271,7 @@ protected function assembleAction(string $action, string $label, string $icon, s protected function getLink(string $action): string { - return Url::fromPath("icingadb/{$this->type}s/$action") + return Url::fromPath($this->getUrlPath() . '/' . $action) ->setFilter($this->getBaseFilter()) ->getAbsoluteUrl(); } From d7cac9def91b65b3e8f95ec2fdc99c135e710c20 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Wed, 13 Nov 2024 16:29:11 +0100 Subject: [PATCH 06/12] RedundancygroupController:: add some optimizations - Use Translation trait's translate() method - Add sort columns to DependencyNode --- .../controllers/RedundancygroupController.php | 74 +++++++++---------- library/Icingadb/Model/DependencyNode.php | 27 ++++++- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index f6ca1a2ec..01048016b 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -6,7 +6,6 @@ use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Common\CommandActions; -use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Data\DependencyNodes; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\RedundancyGroup; @@ -14,7 +13,6 @@ 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\DependencyNodeStatistics; use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupDetail; use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList; @@ -46,7 +44,13 @@ public function init(): void } $this->groupId = $groupId; + } + /** + * Load the redundancy group + */ + protected function loadGroup(): void + { $query = RedundancyGroup::on($this->getDb()) ->with(['state']) ->filter(Filter::equal('id', $this->groupId)); @@ -56,24 +60,18 @@ public function init(): void $this->group = $query->first(); if ($this->group === null) { - throw new NotFoundError(t('Redundancy Group not found')); + throw new NotFoundError($this->translate('Redundancy Group not found')); } $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle($this->group->display_name); $this->addControl(new HtmlElement('div', null, Text::create($this->group->display_name))); - $this->addFooter( - new DependencyNodeStatistics( - RedundancyGroupSummary::on($this->getDb()) - ->filter(Filter::equal('id', $this->groupId)) - ->first() - ) - ); } public function indexAction(): void { + $this->loadGroup(); $summary = RedundancyGroupSummary::on($this->getDb()) ->filter(Filter::equal('id', $this->groupId)); @@ -91,8 +89,9 @@ public function indexAction(): void $this->addContent(new RedundancyGroupDetail($this->group)); } - public function membersAction(): \Generator + public function membersAction(): void { + $this->loadGroup(); $nodesQuery = $this->fetchNodes(true); $limitControl = $this->createLimitControl(); @@ -100,16 +99,17 @@ public function membersAction(): \Generator $sortControl = $this->createSortControl( $nodesQuery, [ - 'name' => t('Name'), - 'severity desc, last_state_change desc' => t('Severity'), - 'state' => t('Current State'), - 'last_state_change desc' => t('Last State Change') + 'name' => $this->translate('Name'), + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); - $searchBar = $this->createSearchBar($nodesQuery, - Links::redundancyGroupMembers($this->group), + $searchBar = $this->createSearchBar( + $nodesQuery, + Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]), [ $limitControl->getLimitParam(), $sortControl->getSortParam(), @@ -132,8 +132,6 @@ public function membersAction(): \Generator $nodesQuery->filter($filter); - yield $this->export($nodesQuery); - $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); @@ -152,8 +150,9 @@ public function membersAction(): \Generator $this->setAutorefreshInterval(10); } - public function childrenAction() + public function childrenAction(): void { + $this->loadGroup(); $nodesQuery = $this->fetchNodes(); $limitControl = $this->createLimitControl(); @@ -161,17 +160,17 @@ public function childrenAction() $sortControl = $this->createSortControl( $nodesQuery, [ - 'name' => t('Name'), - 'severity desc, last_state_change desc' => t('Severity'), - 'state' => t('Current State'), - 'last_state_change desc' => t('Last State Change') + 'name' => $this->translate('Name'), + 'severity desc, last_state_change desc' => $this->translate('Severity'), + 'state' => $this->translate('Current State'), + 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar( $nodesQuery, - Links::redundancyGroupChildren($this->group), + Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]), [ $limitControl->getLimitParam(), $sortControl->getSortParam(), @@ -197,8 +196,6 @@ public function childrenAction() $nodesQuery->filter($filter); - yield $this->export($nodesQuery); - $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); @@ -234,10 +231,11 @@ public function searchEditorAction(): void { $isChildrenTab = $this->params->shift('isChildrenTab'); $redirectUrl = $isChildrenTab - ? Links::redundancyGroupChildren($this->group) - : Links::redundancyGroupMembers($this->group); + ? Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]) + : Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]); - $editor = $this->createSearchEditor(DependencyNode::on($this->getDb()), + $editor = $this->createSearchEditor( + DependencyNode::on($this->getDb()), $redirectUrl, [ LimitControl::DEFAULT_LIMIT_PARAM, @@ -252,23 +250,23 @@ public function searchEditorAction(): void } $this->getDocument()->add($editor); - $this->setTitle(t('Adjust Filter')); + $this->setTitle($this->translate('Adjust Filter')); } protected function createTabs(): Tabs { $tabs = $this->getTabs() ->add('index', [ - 'label' => t('Redundancy Group'), - 'url' => Links::redundancyGroup($this->group) + 'label' => $this->translate('Redundancy Group'), + 'url' => Url::fromPath('icingadb/redundancygroup', ['id' => $this->groupId]) ]) ->add('members', [ - 'label' => t('Members'), - 'url' => Links::redundancyGroupMembers($this->group) + 'label' => $this->translate('Members'), + 'url' => Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]) ]) ->add('children', [ - 'label' => t('Children'), - 'url' => Links::redundancyGroupChildren($this->group) + 'label' => $this->translate('Children'), + 'url' => Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]) ]); return $tabs; @@ -334,7 +332,7 @@ protected function fetchCommandTargets() protected function getCommandTargetsUrl(): Url { - return Links::redundancyGroup($this->group); + return Url::fromPath('icingadb/redundancygroup', ['id' => $this->groupId]); } public function processCheckresultAction(): void diff --git a/library/Icingadb/Model/DependencyNode.php b/library/Icingadb/Model/DependencyNode.php index 635d3a834..a63ec56c8 100644 --- a/library/Icingadb/Model/DependencyNode.php +++ b/library/Icingadb/Model/DependencyNode.php @@ -10,6 +10,7 @@ use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Orm\Relations; +use ipl\Sql\Expression; /** * Dependency node model. @@ -18,6 +19,10 @@ * @property ?string $host_id * @property ?string $service_id * @property ?string $redundancy_group_id + * @property string $name + * @property string $severity + * @property string $state + * @property string $last_state_change * * @property (?Host)|Query $host * @property (?Service)|Query $service @@ -43,7 +48,27 @@ public function getColumns(): array 'id', 'host_id', 'service_id', - 'redundancy_group_id' + 'redundancy_group_id', + 'name' => new Expression( + 'COALESCE(%s, %s, %s)', + ['service.display_name', 'host.display_name', 'redundancy_group.display_name'] + ), + 'severity' => new Expression( + 'COALESCE(%s, %s, %s)', + ['service.state.severity', 'host.state.severity', 'redundancy_group.state.failed'] + ), + 'state' => new Expression( + 'COALESCE(%s, %s, %s)', + ['service.state.soft_state', 'host.state.soft_state', 'redundancy_group.state.failed'] + ), + 'last_state_change' => new Expression( + 'COALESCE(%s, %s, %s)', + [ + 'service.state.last_state_change', + 'host.state.last_state_change', + 'redundancy_group.state.last_state_change' + ] + ), ]; } From 2c5caea31fb58d7d1767e445832779d9bc20b9a9 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Thu, 14 Nov 2024 16:34:00 +0100 Subject: [PATCH 07/12] Introduce `ObjectHeader` class and use it ObjectHeader --- .../controllers/RedundancygroupController.php | 8 +- .../Icingadb/Widget/Detail/ObjectHeader.php | 145 ++++++++++++++++++ .../Widget/Detail/RedundancyGroupHeader.php | 63 ++++++++ public/css/widget/object-header.less | 57 +++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 library/Icingadb/Widget/Detail/ObjectHeader.php create mode 100644 library/Icingadb/Widget/Detail/RedundancyGroupHeader.php create mode 100644 public/css/widget/object-header.less diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index 01048016b..cf40826f0 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -15,6 +15,7 @@ use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupDetail; +use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupHeader; use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList; use ipl\Html\HtmlElement; use ipl\Html\Text; @@ -66,7 +67,12 @@ protected function loadGroup(): void $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle($this->group->display_name); - $this->addControl(new HtmlElement('div', null, Text::create($this->group->display_name))); + $summary = RedundancyGroupSummary::on($this->getDb()) + ->filter(Filter::equal('id', $this->groupId)); + + $this->applyRestrictions($summary); + + $this->addControl(new RedundancyGroupHeader($this->group, $summary->first())); } public function indexAction(): void diff --git a/library/Icingadb/Widget/Detail/ObjectHeader.php b/library/Icingadb/Widget/Detail/ObjectHeader.php new file mode 100644 index 000000000..158622021 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ObjectHeader.php @@ -0,0 +1,145 @@ + */ + protected $baseAttributes = ['class' => 'object-header']; + + /** @var Model The associated object */ + protected $object; + + protected $tag = 'div'; + + /** + * Create a new object header + * + * @param Model $object + */ + public function __construct(Model $object) + { + $this->object = $object; + + $this->addAttributes($this->baseAttributes); + + $this->init(); + } + + abstract protected function assembleHeader(BaseHtmlElement $header): void; + + abstract protected function assembleMain(BaseHtmlElement $main): void; + + protected function assembleCaption(BaseHtmlElement $caption): void + { + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + } + + protected function getStateBallSize(): string + { + return StateBall::SIZE_BIG; + } + + protected function createCaption(): BaseHtmlElement + { + $caption = new HtmlElement('section', Attributes::create(['class' => 'caption'])); + + $this->assembleCaption($caption); + + return $caption; + } + + protected function createHeader(): BaseHtmlElement + { + $header = new HtmlElement('header'); + + $this->assembleHeader($header); + + return $header; + } + + protected function createMain(): BaseHtmlElement + { + $main = new HtmlElement('div', Attributes::create(['class' => 'main'])); + + $this->assembleMain($main); + + return $main; + } + + protected function createTimestamp(): ?BaseHtmlElement + { + //TODO: add support for host/service + return new TimeSince($this->object->state->last_state_change->getTimestamp()); + } + + protected function createSubject(): BaseHtmlElement + { + return new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->object->display_name) + ); + } + + protected function createTitle(): BaseHtmlElement + { + $title = new HtmlElement('div', Attributes::create(['class' => 'title'])); + + $this->assembleTitle($title); + + return $title; + } + + /** + * @return ?BaseHtmlElement + */ + protected function createVisual(): ?BaseHtmlElement + { + $visual = new HtmlElement('div', Attributes::create(['class' => 'visual'])); + + $this->assembleVisual($visual); + if ($visual->isEmpty()) { + return null; + } + + return $visual; + } + + /** + * Initialize the list item + * + * If you want to adjust the object header after construction, override this method. + */ + protected function init(): void + { + } + + protected function assemble(): void + { + $this->add([ + $this->createVisual(), + $this->createMain() + ]); + } +} diff --git a/library/Icingadb/Widget/Detail/RedundancyGroupHeader.php b/library/Icingadb/Widget/Detail/RedundancyGroupHeader.php new file mode 100644 index 000000000..12154c348 --- /dev/null +++ b/library/Icingadb/Widget/Detail/RedundancyGroupHeader.php @@ -0,0 +1,63 @@ +summary = $summary; + + parent::__construct($object); + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $visual->addHtml(new StateBall($this->object->state->getStateText(), $this->getStateBallSize())); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml($this->createSubject()); + if ($this->object->state->failed) { + $text = $this->translate('has no working objects'); + } else { + $text = $this->translate('has working objects'); + } + + $title->addHtml(HtmlElement::create('span', null, Text::create($text))); + } + + protected function createStatistics(): BaseHtmlElement + { + return new DependencyNodeStatistics($this->summary); + } + + protected function assembleHeader(BaseHtmlElement $header): void + { + $header->add($this->createTitle()); + $header->add($this->createStatistics()); + $header->add($this->createTimestamp()); + } + + protected function assembleMain(BaseHtmlElement $main): void + { + $main->add($this->createHeader()); + } +} diff --git a/public/css/widget/object-header.less b/public/css/widget/object-header.less new file mode 100644 index 000000000..9c6be51a8 --- /dev/null +++ b/public/css/widget/object-header.less @@ -0,0 +1,57 @@ +// Layout +.object-header { + display: flex; + + .visual { + display: flex; + padding: 0.5em 0; + align-items: center; + } + + .main { + flex: 1 1 auto; + padding: 0.5em 0; + width: 0; + margin-left: .5em; + + header { + display: flex; + justify-content: space-between; + align-items: center; + + .title { + display: inline-flex; + align-items: baseline; + white-space: nowrap; + margin-right: 1em; + + & > * { + margin: 0 .28125em; // 0 calculated   width + } + + .subject { + .text-ellipsis(); + } + } + + time { + white-space: nowrap; + } + } + } +} + +.object-header { + color: @default-text-color-light; + + .title { + .subject { + color: @default-text-color; + } + } + + .object-statistics { + margin-left: auto; + margin-right: 1em; + } +} From 8ae0f4ae9f3e51a5acaa01f53ed37c9ffdf0577f Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Fri, 15 Nov 2024 13:42:07 +0100 Subject: [PATCH 08/12] RedundancyGroupListItem: make items clickable --- .../ItemList/RedundancyGroupListItem.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index 2b87723d2..ae0147116 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -12,9 +12,10 @@ use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics; use ipl\Html\BaseHtmlElement; use ipl\Stdlib\Filter; +use ipl\Web\Url; +use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; use ipl\Html\HtmlElement; -use ipl\Html\Attributes; use ipl\Html\Text; use ipl\Web\Widget\TimeSince; @@ -33,6 +34,13 @@ class RedundancyGroupListItem extends StateListItem /** @var RedundancyGroupState */ protected $state; + protected function init(): void + { + parent::init(); + + $this->addAttributes(['data-action-item' => true]); + } + protected function getStateBallSize(): string { return StateBall::SIZE_LARGE; @@ -43,12 +51,12 @@ protected function createTimestamp(): BaseHtmlElement return new TimeSince($this->state->last_state_change->getTimestamp()); } - protected function createSubject(): BaseHtmlElement + protected function createSubject(): Link { - return new HtmlElement( - 'span', - Attributes::create(['class' => 'subject']), - Text::create($this->item->display_name) + return new Link( + $this->item->display_name, + Url::fromPath('icingadb/redundancygroup', ['id' => bin2hex($this->item->id)]), + ['class' => 'subject'] ); } From 2fb2c93ef847ea2a050bfcf957ef1907f79fa5e7 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Fri, 15 Nov 2024 13:44:35 +0100 Subject: [PATCH 09/12] RedundancyGroupListItem: Apply restrictions to summary query - Add type hint for $state with @property tag instead --- .../Widget/ItemList/RedundancyGroupListItem.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index ae0147116..eca3c9271 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Icingadb\Widget\ItemList; +use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\ListItemCommonLayout; use Icinga\Module\Icingadb\Model\RedundancyGroup; @@ -23,17 +24,16 @@ * Redundancy group list item. Represents one database row. * * @property RedundancyGroup $item + * @property RedundancyGroupState $state */ class RedundancyGroupListItem extends StateListItem { use ListItemCommonLayout; use Database; + use Auth; protected $defaultAttributes = ['class' => ['redundancy-group-list-item']]; - /** @var RedundancyGroupState */ - protected $state; - protected function init(): void { parent::init(); @@ -67,11 +67,12 @@ protected function assembleVisual(BaseHtmlElement $visual): void protected function assembleCaption(BaseHtmlElement $caption): void { - $caption->addHtml(new DependencyNodeStatistics( - RedundancyGroupSummary::on($this->getDb()) - ->filter(Filter::equal('id', $this->item->id)) - ->first() - )); + $summary = RedundancyGroupSummary::on($this->getDb()) + ->filter(Filter::equal('id', $this->item->id)); + + $this->applyRestrictions($summary); + + $caption->addHtml(new DependencyNodeStatistics($summary->first())); } protected function assembleTitle(BaseHtmlElement $title): void From f4b4d9202135dffc21a38e05f869cae084cb32ad Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Mon, 18 Nov 2024 15:28:08 +0100 Subject: [PATCH 10/12] RedundancygroupController: Remove query `execute()` call RedundancygroupController: Add class property for group summary RedundancygroupController: Apply missing restrictions in `fetchNodes()` method rg: Use $this->httpNotFound instead rg: remove superfluous searchbar param --- .../controllers/RedundancygroupController.php | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index cf40826f0..159b2e33c 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -36,6 +36,9 @@ class RedundancygroupController extends Controller /** @var RedundancyGroup */ protected $group; + /** @var RedundancyGroupSummary */ + protected $groupSummary; + public function init(): void { // in case of quick actions, param id is not given @@ -61,7 +64,7 @@ protected function loadGroup(): void $this->group = $query->first(); if ($this->group === null) { - throw new NotFoundError($this->translate('Redundancy Group not found')); + $this->httpNotFound($this->translate('Redundancy Group not found')); } $this->setTitleTab($this->getRequest()->getActionName()); @@ -72,20 +75,18 @@ protected function loadGroup(): void $this->applyRestrictions($summary); - $this->addControl(new RedundancyGroupHeader($this->group, $summary->first())); + $this->groupSummary = $summary->first(); + + $this->addControl(new RedundancyGroupHeader($this->group, $this->groupSummary)); } public function indexAction(): void { $this->loadGroup(); - $summary = RedundancyGroupSummary::on($this->getDb()) - ->filter(Filter::equal('id', $this->groupId)); - - $this->filter($summary); // The base filter is required to fetch the correct objects for MultiselectQuickActions::isGrantedOnType() check $this->addControl( - (new MultiselectQuickActions('dependency_node', $summary->first())) + (new MultiselectQuickActions('dependency_node', $this->groupSummary)) ->setBaseFilter(Filter::equal('child.redundancy_group.id', $this->groupId)) ->setAllowToProcessCheckResults(false) ->setColumnPrefix('nodes') @@ -115,7 +116,6 @@ public function membersAction(): void $searchBar = $this->createSearchBar( $nodesQuery, - Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]), [ $limitControl->getLimitParam(), $sortControl->getSortParam(), @@ -145,7 +145,7 @@ public function membersAction(): void $this->addControl($searchBar); $this->addContent( - (new DependencyNodeList($nodesQuery->execute())) + (new DependencyNodeList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ); @@ -176,7 +176,6 @@ public function childrenAction(): void $searchBar = $this->createSearchBar( $nodesQuery, - Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]), [ $limitControl->getLimitParam(), $sortControl->getSortParam(), @@ -209,7 +208,7 @@ public function childrenAction(): void $this->addControl($searchBar); $this->addContent( - (new DependencyNodeList($nodesQuery->execute())) + (new DependencyNodeList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ); @@ -301,7 +300,7 @@ private function fetchNodes(bool $fetchParents = false): Query $fetchParents ? 'child' : 'parent' ); - return DependencyNode::on($this->getDb()) + $query = DependencyNode::on($this->getDb()) ->with([ 'host', 'host.state', @@ -311,6 +310,10 @@ private function fetchNodes(bool $fetchParents = false): Query 'service.host.state' ]) ->filter(Filter::equal($filterColumn, $this->groupId)); + + $this->applyRestrictions($query); + + return $query; } protected function fetchCommandTargets() From 6455e3b8e2d3b90b7d4899b95fca76e3bf21ab93 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 19 Nov 2024 11:42:01 +0100 Subject: [PATCH 11/12] DependencyNodeList: Add `viewModeSwicher` functionality for hosts/services - Add missing relation `state.last_comment` for detailed view mode --- .../controllers/RedundancygroupController.php | 2 ++ .../Widget/ItemList/DependencyNodeList.php | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index 159b2e33c..cd3107fa2 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -304,8 +304,10 @@ private function fetchNodes(bool $fetchParents = false): Query ->with([ 'host', 'host.state', + 'host.state.last_comment', 'service', 'service.state', + 'service.state.last_comment', 'service.host', 'service.host.state' ]) diff --git a/library/Icingadb/Widget/ItemList/DependencyNodeList.php b/library/Icingadb/Widget/ItemList/DependencyNodeList.php index 04dfc6ffa..c95f883fa 100644 --- a/library/Icingadb/Widget/ItemList/DependencyNodeList.php +++ b/library/Icingadb/Widget/ItemList/DependencyNodeList.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Module\Icingadb\Model\DependencyNode; +use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\UnreachableParent; use ipl\Web\Common\BaseListItem; @@ -30,10 +31,23 @@ protected function createListItem(object $data): BaseListItem /** @var UnreachableParent|DependencyNode $data */ if ($data->redundancy_group_id !== null) { return new RedundancyGroupListItem($data->redundancy_group, $this); - } elseif ($data->service_id !== null) { - return new ServiceListItem($data->service, $this); - } else { - return new HostListItem($data->host, $this); } + + $object = $data->service_id !== null ? $data->service : $data->host; + + switch ($this->getViewMode()) { + case 'minimal': + $class = $object instanceof Host ? HostListItemMinimal::class : ServiceListItemMinimal::class; + break; + case 'detailed': + $this->removeAttribute('class', 'default-layout'); + + $class = $object instanceof Host ? HostListItemDetailed::class : ServiceListItemDetailed::class; + break; + default: + $class = $object instanceof Host ? HostListItem::class : ServiceListItem::class; + } + + return new $class($object, $this); } } From 3567a406c719b2af76e1a586401477af6ed77672 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 19 Nov 2024 17:34:23 +0100 Subject: [PATCH 12/12] RedundancygroupController: Fix that `isChildrenTab` apear in the browser url --- application/controllers/RedundancygroupController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/controllers/RedundancygroupController.php b/application/controllers/RedundancygroupController.php index cd3107fa2..9e0c65fb0 100644 --- a/application/controllers/RedundancygroupController.php +++ b/application/controllers/RedundancygroupController.php @@ -185,7 +185,8 @@ public function childrenAction(): void ); $searchBar->getSuggestionUrl()->setParam('isChildrenTab'); - $searchBar->getEditorUrl()->setParam('isChildrenTab'); + $searchBar->getEditorUrl() + ->setParams((clone $searchBar->getEditorUrl()->getParams())->set('isChildrenTab', true)); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) {