From e77c687c9454aaf6f60767d83f62eb50fb603a64 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Tue, 17 Sep 2024 16:15:26 +0200 Subject: [PATCH] Show root problem list for objects with problem and are part of dependency --- library/Icingadb/Common/StateBadges.php | 88 ++++++---- .../RedundancyGroupParentStateSummary.php | 111 +++++++++++++ library/Icingadb/Widget/Detail/HostDetail.php | 3 +- .../Icingadb/Widget/Detail/ObjectDetail.php | 34 +++- .../Icingadb/Widget/Detail/ServiceDetail.php | 3 +- .../ItemList/RedundancyGroupListItem.php | 153 ++++++++++++++++++ .../Widget/ItemList/RootProblemList.php | 34 ++++ .../RedundancyGroupParentStateBadges.php | 33 ++++ .../Widget/RedundancyGroupStatistics.php | 52 ++++++ public/css/list/root-problem-list.less | 31 ++++ .../redundancy-group-parent-state-badges.less | 3 + 11 files changed, 511 insertions(+), 34 deletions(-) create mode 100644 library/Icingadb/Model/RedundancyGroupParentStateSummary.php create mode 100644 library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php create mode 100644 library/Icingadb/Widget/ItemList/RootProblemList.php create mode 100644 library/Icingadb/Widget/RedundancyGroupParentStateBadges.php create mode 100644 library/Icingadb/Widget/RedundancyGroupStatistics.php create mode 100644 public/css/list/root-problem-list.less create mode 100644 public/css/widget/redundancy-group-parent-state-badges.less diff --git a/library/Icingadb/Common/StateBadges.php b/library/Icingadb/Common/StateBadges.php index c9c5c89f7..16ae6eb85 100644 --- a/library/Icingadb/Common/StateBadges.php +++ b/library/Icingadb/Common/StateBadges.php @@ -46,13 +46,6 @@ public function __construct($item) $this->url = $this->getBaseUrl(); } - /** - * Get the badge base URL - * - * @return Url - */ - abstract protected function getBaseUrl(): Url; - /** * Get the type of the items * @@ -67,6 +60,16 @@ abstract protected function getType(): string; */ abstract protected function getPrefix(): string; + /** + * Get the badge base URL + * + * @return ?Url + */ + protected function getBaseUrl(): ?Url + { + return null; + } + /** * Get the integer of the given state text * @@ -74,7 +77,10 @@ abstract protected function getPrefix(): string; * * @return int */ - abstract protected function getStateInt(string $state): int; + protected function getStateInt(string $state): int + { + return 0; + } /** * Get the badge URL @@ -132,18 +138,25 @@ public function createLink($content, Filter\Rule $filter = null): Link * Create a state bade * * @param string $state + * @param bool $createLink Create link for the badge if true * * @return ?BaseHtmlElement */ - protected function createBadge(string $state) + protected function createBadge(string $state, bool $createLink = true) { $key = $this->prefix . "_{$state}"; if (isset($this->item->$key) && $this->item->$key) { - return Html::tag('li', $this->createLink( - new StateBadge($this->item->$key, $state), - Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)) - )); + $stateBadge = new StateBadge($this->item->$key, $state); + + if ($createLink) { + $this->createLink( + $stateBadge, + Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)) + ); + } + + return Html::tag('li', $stateBadge); } return null; @@ -153,37 +166,50 @@ protected function createBadge(string $state) * Create a state group * * @param string $state + * @param bool $createLink Create link for the badge if true * * @return ?BaseHtmlElement */ - protected function createGroup(string $state) + protected function createGroup(string $state, bool $createLink = true) { $content = []; $handledKey = $this->prefix . "_{$state}_handled"; $unhandledKey = $this->prefix . "_{$state}_unhandled"; if (isset($this->item->$unhandledKey) && $this->item->$unhandledKey) { - $content[] = Html::tag('li', $this->createLink( - new StateBadge($this->item->$unhandledKey, $state), - Filter::all( - Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), - Filter::equal($this->type . '.state.is_handled', 'n'), - Filter::equal($this->type . '.state.is_reachable', 'y') - ) - )); + $unhandledStateBadge = new StateBadge($this->item->$unhandledKey, $state); + + if ($createLink) { + $unhandledStateBadge = $this->createLink( + $unhandledStateBadge, + Filter::all( + Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), + Filter::equal($this->type . '.state.is_handled', 'n'), + Filter::equal($this->type . '.state.is_reachable', 'y') + ) + ); + } + + $content[] = Html::tag('li', $unhandledStateBadge); } if (isset($this->item->$handledKey) && $this->item->$handledKey) { - $content[] = Html::tag('li', $this->createLink( - new StateBadge($this->item->$handledKey, $state, true), - Filter::all( - Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), - Filter::any( - Filter::equal($this->type . '.state.is_handled', 'y'), - Filter::equal($this->type . '.state.is_reachable', 'n') + $handledStateBadge = new StateBadge($this->item->$handledKey, $state, true); + + if ($createLink) { + $handledStateBadge = $this->createLink( + $handledStateBadge, + Filter::all( + Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), + Filter::any( + Filter::equal($this->type . '.state.is_handled', 'y'), + Filter::equal($this->type . '.state.is_reachable', 'n') + ) ) - ) - )); + ); + } + + $content[] = Html::tag('li', $handledStateBadge); } if (empty($content)) { diff --git a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php new file mode 100644 index 000000000..e9945a3b2 --- /dev/null +++ b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php @@ -0,0 +1,111 @@ + new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host_state.soft_state = 1' + . ' AND (redundancy_group_from_to_host_state.is_handled = \'y\'' + . ' OR redundancy_group_from_to_host_state.is_reachable = \'n\') THEN 1 ELSE 0 END' + . '+ CASE WHEN redundancy_group_from_to_service_state.soft_state = 2' + . ' AND (redundancy_group_from_to_service_state.is_handled = \'y\'' + . ' OR redundancy_group_from_to_service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' + ), + 'parents_problem_unhandled' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host_state.soft_state = 1' + . ' AND redundancy_group_from_to_host_state.is_handled = \'n\'' + . ' AND redundancy_group_from_to_host_state.is_reachable = \'y\' THEN 1 ELSE 0 END' + . ' + CASE WHEN redundancy_group_from_to_service_state.soft_state = 2' + . ' AND redundancy_group_from_to_service_state.is_handled = \'n\'' + . ' AND redundancy_group_from_to_service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' + ), + 'parents_pending' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host_state.soft_state = 99 THEN 1 ELSE 0 END' + . '+ CASE WHEN redundancy_group_from_to_service_state.soft_state = 99 THEN 1 ELSE 0 END)' + ), + 'parents_problems_unacknowledged' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host_state.is_problem = \'y\'' + . ' AND redundancy_group_from_to_host_state.is_acknowledged = \'n\' THEN 1 ELSE 0 END' + . '+ CASE WHEN redundancy_group_from_to_service_state.is_problem = \'y\'' + . ' AND redundancy_group_from_to_service_state.is_acknowledged = \'n\' THEN 1 ELSE 0 END)' + ), + 'parents_total' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host.id IS NOT NULL THEN 1 ELSE 0 END)' + . '+ SUM(CASE WHEN redundancy_group_from_to_service.id IS NOT NULL THEN 1 ELSE 0 END)' + ), + 'parents_ok' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_host_state.soft_state = 0 THEN 1 ELSE 0 END' + . '+ CASE WHEN redundancy_group_from_to_service_state.soft_state = 0 THEN 1 ELSE 0 END)' + ), + 'parents_unknown_handled' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_service_state.soft_state = 3' + . ' AND (redundancy_group_from_to_service_state.is_handled = \'y\'' + . ' OR redundancy_group_from_to_service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' + ), + 'parents_unknown_unhandled' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_service_state.soft_state = 3' + . ' AND redundancy_group_from_to_service_state.is_handled = \'n\'' + . ' AND redundancy_group_from_to_service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' + ), + 'parents_warning_handled' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_service_state.soft_state = 1' + . ' AND (redundancy_group_from_to_service_state.is_handled = \'y\'' + . ' OR redundancy_group_from_to_service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' + ), + 'parents_warning_unhandled' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_service_state.soft_state = 1' + . ' AND redundancy_group_from_to_service_state.is_handled = \'n\'' + . ' AND redundancy_group_from_to_service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' + ) + ]; + } + + public static function on(Connection $db) + { + $q = parent::on($db)->with([ + 'from', + 'from.to.host', + 'from.to.host.state', + 'from.to.service', + 'from.to.service.state' + ]); + + /** @var static $m */ + $m = $q->getModel(); + $q->columns($m->getSummaryColumns()); + + return $q; + } + + public function getColumns(): array + { + return array_merge(parent::getColumns(), $this->getSummaryColumns()); + } + + public function getDefaultSort() + { + return null; + } +} diff --git a/library/Icingadb/Widget/Detail/HostDetail.php b/library/Icingadb/Widget/Detail/HostDetail.php index 8b80480ac..969b37ffb 100644 --- a/library/Icingadb/Widget/Detail/HostDetail.php +++ b/library/Icingadb/Widget/Detail/HostDetail.php @@ -41,7 +41,8 @@ protected function assemble() } $this->add(ObjectDetailExtensionHook::injectExtensions([ - 0 => $this->createPluginOutput(), + 0 => $this->createRootProblems(), + 1 => $this->createPluginOutput(), 190 => $this->createServiceStatistics(), 300 => $this->createActions(), 301 => $this->createNotes(), diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 58edac6b2..39ead2ddf 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -20,9 +20,10 @@ use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\Macros; use Icinga\Module\Icingadb\Compat\CompatHost; -use Icinga\Module\Icingadb\Compat\CompatService; use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Web\Navigation\Action; +use Icinga\Module\Icingadb\Widget\ItemList\RootProblemList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; @@ -602,4 +603,35 @@ protected function fetchCustomVars() $this->object->customvar_flat = $customvarFlat->execute(); } } + + protected function createRootProblems(): ?array + { + $rootProblems = UnreachableParent::on($this->getDb(), $this->object) + ->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' + ]); + + $this->applyRestrictions($rootProblems); + + if ($rootProblems->count() > 0) { + $rootProblemList = (new RootProblemList( + $rootProblems->execute() + )); + + return [HtmlElement::create('h2', null, t('Root Problems')), $rootProblemList]; + } + + return null; + } } diff --git a/library/Icingadb/Widget/Detail/ServiceDetail.php b/library/Icingadb/Widget/Detail/ServiceDetail.php index 8421e314f..86e7651ff 100644 --- a/library/Icingadb/Widget/Detail/ServiceDetail.php +++ b/library/Icingadb/Widget/Detail/ServiceDetail.php @@ -21,7 +21,8 @@ protected function assemble() } $this->add(ObjectDetailExtensionHook::injectExtensions([ - 0 => $this->createPluginOutput(), + 0 => $this->createRootProblems(), + 1 => $this->createPluginOutput(), 300 => $this->createActions(), 301 => $this->createNotes(), 400 => $this->createComments(), diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php new file mode 100644 index 000000000..956bcd85c --- /dev/null +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -0,0 +1,153 @@ +state->last_state_change->getTimestamp()); + } + + protected function createSubject(): BaseHtmlElement + { + return new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ); + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $stateText = $this->state->failed ? 'problem' : 'ok'; + $stateBall = new StateBall($stateText, $this->getStateBallSize()); + + $visual->addHtml($stateBall); + } + + protected function assembleCaption(BaseHtmlElement $caption): void + { + $filter = Filter::equal('id', $this->item->id); + $relations = [ + 'from', + 'from.to.host', + 'from.to.host.state', + 'from.to.service', + 'from.to.service.state' + ]; + + $summary = RedundancyGroupParentStateSummary::on($this->getDb()) + ->with($relations) + ->filter($filter); + + $members = RedundancyGroup::on($this->getDb()) + ->columns([ + 'id' => 'id', + 'parent_output' => new Expression( + 'CASE WHEN redundancy_group_from_to_host_state.output IS NULL' + . ' THEN redundancy_group_from_to_service_state.output' + . ' ELSE redundancy_group_from_to_host_state.output END' + ), + 'parent_long_output' => new Expression( + 'CASE WHEN redundancy_group_from_to_host_state.long_output IS NULL' + . ' THEN redundancy_group_from_to_service_state.long_output' + . ' ELSE redundancy_group_from_to_host_state.long_output END' + ), + 'parent_checkcommand_name' => new Expression( + 'CASE WHEN redundancy_group_from_to_host.checkcommand_name IS NULL' + . ' THEN redundancy_group_from_to_service.checkcommand_name' + . ' ELSE redundancy_group_from_to_host.checkcommand_name END' + ), + 'parent_last_state_change' => new Expression( + 'CASE WHEN redundancy_group_from_to_host_state.last_state_change IS NULL' + . ' THEN redundancy_group_from_to_service_state.last_state_change' + . ' ELSE redundancy_group_from_to_host_state.last_state_change END' + ), + 'parent_severity' => new Expression( + 'CASE WHEN redundancy_group_from_to_host_state.severity IS NULL' + . ' THEN redundancy_group_from_to_service_state.severity' + . ' ELSE redundancy_group_from_to_host_state.severity END' + ) + ]) + ->with($relations) + ->filter($filter) + ->orderBy([ + 'parent_severity', + 'parent_last_state_change', + ], 'DESC'); + + $this->applyRestrictions($members); + + /** @var RedundancyGroup $data */ + $data = $members->first(); + + if($data) { + $caption->addHtml(new PluginOutputContainer( + (new PluginOutput($data->parent_output . "\n" .$data->parent_long_output)) + ->setCommandName($data->parent_checkcommand_name) + )); + } + + $caption->addHtml(new RedundancyGroupStatistics($summary->first())); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + if ($this->state->failed) { + $verb = 'has'; + } else { + $verb = 'is'; + } + + $title->addHtml(Html::sprintf( + t('%s %s %s', ' has/is '), + $this->createSubject(), + $verb, + Html::tag('span', ['class' => 'state-text'], $this->state->failed ? 'Failed' : 'OK') + )); + } + + protected function assemble(): void + { + $this->add([ + $this->createVisual(), + $this->createIconImage(), + $this->createMain() + ]); + } +} diff --git a/library/Icingadb/Widget/ItemList/RootProblemList.php b/library/Icingadb/Widget/ItemList/RootProblemList.php new file mode 100644 index 000000000..2011c8156 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/RootProblemList.php @@ -0,0 +1,34 @@ + ['root-problem-list']]; + + protected function init(): void + { + $this->initializeDetailActions(); + } + + protected function getItemClass(): string + { + return ''; + } + + protected function createListItem(object $data) + { + /** @var UnreachableParent $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); + } + } +} diff --git a/library/Icingadb/Widget/RedundancyGroupParentStateBadges.php b/library/Icingadb/Widget/RedundancyGroupParentStateBadges.php new file mode 100644 index 000000000..e5970ee82 --- /dev/null +++ b/library/Icingadb/Widget/RedundancyGroupParentStateBadges.php @@ -0,0 +1,33 @@ +addAttributes(['class' => 'redundancy-group-parent-state-badges']); + + $this->add(array_filter([ + $this->createGroup('problem', false), + $this->createGroup('warning', false), + $this->createGroup('unknown', false), + $this->createBadge('ok', false), + $this->createBadge('pending', false) + ])); + } +} diff --git a/library/Icingadb/Widget/RedundancyGroupStatistics.php b/library/Icingadb/Widget/RedundancyGroupStatistics.php new file mode 100644 index 000000000..4a62a91d1 --- /dev/null +++ b/library/Icingadb/Widget/RedundancyGroupStatistics.php @@ -0,0 +1,52 @@ +summary = $summary; + } + + protected function createDonut(): ValidHtml + { + $donut = (new Donut()) + ->addSlice($this->summary->parents_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->parents_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->parents_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->parents_problem_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->parents_problem_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->parents_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->parents_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->parents_pending, ['class' => 'slice-state-pending']); + + return HtmlString::create($donut->render()); + } + + protected function createTotal(): ValidHtml + { + return Text::create($this->summary->parents_total); + } + + protected function createBadges(): ValidHtml + { + $badges = new RedundancyGroupParentStateBadges($this->summary); + if ($this->hasBaseFilter()) { + $badges->setBaseFilter($this->getBaseFilter()); + } + + return $badges; + } +} diff --git a/public/css/list/root-problem-list.less b/public/css/list/root-problem-list.less new file mode 100644 index 000000000..5b1e07e53 --- /dev/null +++ b/public/css/list/root-problem-list.less @@ -0,0 +1,31 @@ +.root-problem-list.item-list { + .list-item { + .caption { + .plugin-output { + background: none; + } + + display: flex; + justify-content: space-between; + + .plugin-output { + vertical-align: middle; + .text-ellipsis(); + } + + .object-statistics { + ul { + display: flex; + } + + .state-badges { + font-size: 0.75em; + } + } + } + + .state-problem { + background-color: @state-critical; + } + } +} diff --git a/public/css/widget/redundancy-group-parent-state-badges.less b/public/css/widget/redundancy-group-parent-state-badges.less new file mode 100644 index 000000000..31e3d5c1c --- /dev/null +++ b/public/css/widget/redundancy-group-parent-state-badges.less @@ -0,0 +1,3 @@ +.redundancy-group-parent-state-badges { + .state-badges(); +}