From 07b821a672cb61678f53bef3c44ea2392a630771 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Tue, 17 Sep 2024 16:15:26 +0200 Subject: [PATCH 01/13] Show root problem list for objects with problem and are part of dependency --- library/Icingadb/Common/StateBadges.php | 93 +++++---- .../RedundancyGroupParentStateSummary.php | 101 ++++++++++ library/Icingadb/Widget/Detail/HostDetail.php | 3 +- .../Icingadb/Widget/Detail/ObjectDetail.php | 43 ++++- .../Icingadb/Widget/Detail/ServiceDetail.php | 3 +- .../Widget/ItemList/DependencyNodeList.php | 39 ++++ .../ItemList/RedundancyGroupListItem.php | 176 ++++++++++++++++++ .../Icingadb/Widget/ObjectsStateBadges.php | 36 ++++ library/Icingadb/Widget/ObjectsStatistics.php | 56 ++++++ public/css/common.less | 18 +- .../css/list/redundancy-group-list-item.less | 21 +++ public/css/widget/objects-state-badges.less | 3 + 12 files changed, 554 insertions(+), 38 deletions(-) create mode 100644 library/Icingadb/Model/RedundancyGroupParentStateSummary.php create mode 100644 library/Icingadb/Widget/ItemList/DependencyNodeList.php create mode 100644 library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php create mode 100644 library/Icingadb/Widget/ObjectsStateBadges.php create mode 100644 library/Icingadb/Widget/ObjectsStatistics.php create mode 100644 public/css/list/redundancy-group-list-item.less create mode 100644 public/css/widget/objects-state-badges.less diff --git a/library/Icingadb/Common/StateBadges.php b/library/Icingadb/Common/StateBadges.php index c9c5c89f7..2836407bc 100644 --- a/library/Icingadb/Common/StateBadges.php +++ b/library/Icingadb/Common/StateBadges.php @@ -4,11 +4,12 @@ namespace Icinga\Module\Icingadb\Common; +use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Html\HtmlElement; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; -use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBadge; @@ -26,7 +27,7 @@ abstract class StateBadges extends BaseHtmlElement /** @var string Prefix */ protected $prefix; - /** @var Url Badge link */ + /** @var ?Url Badge link */ protected $url; protected $tag = 'ul'; @@ -46,13 +47,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,14 +61,29 @@ 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 * * @param string $state * * @return int + * + * @throws InvalidArgumentException if the given state is not valid */ - abstract protected function getStateInt(string $state): int; + protected function getStateInt(string $state): int + { + throw new InvalidArgumentException('%s is not a valid state', $state); + } /** * Get the badge URL @@ -135,15 +144,21 @@ public function createLink($content, Filter\Rule $filter = null): Link * * @return ?BaseHtmlElement */ - protected function createBadge(string $state) + protected function createBadge(string $state): ?BaseHtmlElement { $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 ($this->url !== null) { + $this->createLink( + $stateBadge, + Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)) + ); + } + + return new HtmlElement('li', null, $stateBadge); } return null; @@ -156,34 +171,46 @@ protected function createBadge(string $state) * * @return ?BaseHtmlElement */ - protected function createGroup(string $state) + protected function createGroup(string $state): ?BaseHtmlElement { $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 ($this->url !== null) { + $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[] = new HtmlElement('li', null, $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 ($this->url !== null) { + $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[] = new HtmlElement('li', null, $handledStateBadge); } if (empty($content)) { diff --git a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php new file mode 100644 index 000000000..9f9b58d70 --- /dev/null +++ b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php @@ -0,0 +1,101 @@ + 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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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)' + ), + 'objects_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): Query + { + $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()); + } +} 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..285d374f0 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\DependencyNodeList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; @@ -442,7 +443,7 @@ protected function createPluginOutput(): array 'div', [ 'id' => 'check-output-' . $this->object->checkcommand_name, - 'class' => 'collapsible', + 'class' => ['collapsible', 'check-command-output'], 'data-visible-height' => 100 ], $pluginOutput @@ -602,4 +603,42 @@ protected function fetchCustomVars() $this->object->customvar_flat = $customvarFlat->execute(); } } + + protected function createRootProblems(): ?array + { + if ($this->object->state->is_reachable) { + return null; + } + + $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' + ])->orderBy([ + 'host.state.severity' => SORT_DESC, + 'host.state.last_state_change' => SORT_DESC, + 'service.state.severity' => SORT_DESC, + 'service.state.last_state_change' => SORT_DESC, + 'service.host.state.severity' => SORT_DESC, + 'service.host.state.last_state_change' => SORT_DESC, + 'redundancy_group.state.last_state_change' => SORT_DESC + ]); + + $this->applyRestrictions($rootProblems); + + return [ + HtmlElement::create('h2', null, Text::create(t('Root Problems'))), + new DependencyNodeList($rootProblems) + ]; + } } 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/DependencyNodeList.php b/library/Icingadb/Widget/ItemList/DependencyNodeList.php new file mode 100644 index 000000000..462e0a3ac --- /dev/null +++ b/library/Icingadb/Widget/ItemList/DependencyNodeList.php @@ -0,0 +1,39 @@ + ['root-problem-list']]; + + protected function init(): void + { + $this->initializeDetailActions(); + } + + protected function getItemClass(): string + { + return ''; + } + + 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); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php new file mode 100644 index 000000000..cf162f24a --- /dev/null +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -0,0 +1,176 @@ + ['list-item', 'redundancy-group-list-item']]; + + /** @var RedundancyGroupParentStateSummary Objects state summary */ + protected $summary; + + /** @var RedundancyGroupState */ + protected $state; + + /** @var bool Whether the redundancy group has been handled */ + protected $isHandled = false; + + protected function init(): void + { + parent::init(); + + $this->summary = RedundancyGroupParentStateSummary::on($this->getDb()) + ->with([ + 'from', + 'from.to.host', + 'from.to.host.state', + 'from.to.service', + 'from.to.service.state' + ]) + ->filter(Filter::equal('id', $this->item->id)) + ->first(); + + $this->isHandled = $this->state->failed + && ( + $this->summary->objects_problem_handled + || $this->summary->objects_unknown_handled + || $this->summary->objects_warning_handled + ); + } + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } + + protected function createTimestamp(): ?BaseHtmlElement + { + return new TimeSince($this->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 + { + $stateBall = new StateBall($this->state->getStateText(), $this->getStateBallSize()); + if ($this->isHandled) { + $stateBall->getAttributes()->add('class', 'handled'); + } + + $visual->addHtml($stateBall); + } + + protected function assembleCaption(BaseHtmlElement $caption): void + { + $members = RedundancyGroup::on($this->getDb()) + ->columns([ + 'id' => 'id', + 'objects_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' + ), + 'objects_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' + ), + 'objects_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' + ), + 'objects_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' + ), + 'objects_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([ + 'from', + 'from.to.host', + 'from.to.host.state', + 'from.to.service', + 'from.to.service.state' + ]) + ->filter(Filter::equal('id', $this->item->id)) + ->orderBy([ + 'objects_severity', + 'objects_last_state_change', + ], SORT_DESC); + + $this->applyRestrictions($members); + + /** @var RedundancyGroup $data */ + $data = $members->first(); + + if ($data) { + $caption->addHtml(new PluginOutputContainer( + (new PluginOutput($data->objects_output . "\n" . $data->objects_long_output)) + ->setCommandName($data->objects_checkcommand_name) + )); + } + + $caption->addHtml(new ObjectsStatistics($this->summary)); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml($this->createSubject()); + if ($this->state->failed) { + $title->addHtml(HtmlElement::create('span', null, Text::create(t('has no working objects')))); + } else { + $title->addHtml(HtmlElement::create('span', null, Text::create(t('has working objects')))); + } + } + + protected function assemble(): void + { + $this->add([ + $this->createVisual(), + $this->createIconImage(), + $this->createMain() + ]); + } +} diff --git a/library/Icingadb/Widget/ObjectsStateBadges.php b/library/Icingadb/Widget/ObjectsStateBadges.php new file mode 100644 index 000000000..0a751ad4b --- /dev/null +++ b/library/Icingadb/Widget/ObjectsStateBadges.php @@ -0,0 +1,36 @@ +addAttributes(['class' => 'objects-state-badges']); + + $this->add(array_filter([ + $this->createGroup('problem'), + $this->createGroup('warning'), + $this->createGroup('unknown'), + $this->createBadge('ok'), + $this->createBadge('pending') + ])); + } +} diff --git a/library/Icingadb/Widget/ObjectsStatistics.php b/library/Icingadb/Widget/ObjectsStatistics.php new file mode 100644 index 000000000..d32eb4b4b --- /dev/null +++ b/library/Icingadb/Widget/ObjectsStatistics.php @@ -0,0 +1,56 @@ +summary = $summary; + } + + protected function createDonut(): ValidHtml + { + $donut = (new Donut()) + ->addSlice($this->summary->objects_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->objects_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->objects_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->objects_problem_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->objects_problem_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->objects_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->objects_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->objects_pending, ['class' => 'slice-state-pending']); + + return HtmlString::create($donut->render()); + } + + protected function createTotal(): ValidHtml + { + return Text::create($this->shortenAmount($this->summary->objects_total)); + } + + protected function createBadges(): ValidHtml + { + $badges = new ObjectsStateBadges($this->summary); + if ($this->hasBaseFilter()) { + $badges->setBaseFilter($this->getBaseFilter()); + } + + return $badges; + } +} diff --git a/public/css/common.less b/public/css/common.less index 39da032f8..ee9d1e50f 100644 --- a/public/css/common.less +++ b/public/css/common.less @@ -197,7 +197,7 @@ div.show-more { margin-left: 1em / 1.333em; // 1em / h2 font size } -.object-detail .plugin-output { +.object-detail .check-command-output .plugin-output { .rounded-corners(.25em); background-color: @gray-lighter; padding: .5em; @@ -412,3 +412,19 @@ form[name="form_confirm_removal"] { padding: 0 0.25em; .rounded-corners(); } + +.state-ball { + &.state-unreachable { + background-color: @color-critical; + } + + &.state-reachable { + background-color: @color-ok; + } +} + +.state-badge { + &.state-problem { + background-color: @color-critical; + } +} diff --git a/public/css/list/redundancy-group-list-item.less b/public/css/list/redundancy-group-list-item.less new file mode 100644 index 000000000..431b44717 --- /dev/null +++ b/public/css/list/redundancy-group-list-item.less @@ -0,0 +1,21 @@ +.redundancy-group-list-item.list-item { + .caption { + display: flex; + justify-content: space-between; + + .plugin-output { + vertical-align: middle; + .text-ellipsis(); + } + + .object-statistics { + ul { + display: flex; + } + + .state-badges { + font-size: 0.75em; + } + } + } +} diff --git a/public/css/widget/objects-state-badges.less b/public/css/widget/objects-state-badges.less new file mode 100644 index 000000000..ad369c66a --- /dev/null +++ b/public/css/widget/objects-state-badges.less @@ -0,0 +1,3 @@ +.objects-state-badges { + .state-badges(); +} From d1d871c9d1858294cda7097091339a576a64a7ce Mon Sep 17 00:00:00 2001 From: raviks789 Date: Thu, 26 Sep 2024 12:11:36 +0200 Subject: [PATCH 02/13] Add `HasRootProblem` behavior for service --- application/controllers/ServiceController.php | 16 ++-- .../Model/Behavior/HasRootProblem.php | 80 +++++++++++++++++++ library/Icingadb/Model/Service.php | 3 + .../Icingadb/Widget/Detail/ObjectDetail.php | 6 +- 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 library/Icingadb/Model/Behavior/HasRootProblem.php diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index 8867e9185..90b3a4412 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -38,13 +38,15 @@ public function init() $name = $this->params->getRequired('name'); $hostName = $this->params->getRequired('host.name'); - $query = Service::on($this->getDb())->with([ - 'state', - 'icon_image', - 'host', - 'host.state', - 'timeperiod' - ]); + $query = Service::on($this->getDb()) + ->withColumns(['has_root_problem']) + ->with([ + 'state', + 'icon_image', + 'host', + 'host.state', + 'timeperiod' + ]); $query ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::all( diff --git a/library/Icingadb/Model/Behavior/HasRootProblem.php b/library/Icingadb/Model/Behavior/HasRootProblem.php new file mode 100644 index 000000000..71d0d09a9 --- /dev/null +++ b/library/Icingadb/Model/Behavior/HasRootProblem.php @@ -0,0 +1,80 @@ +query = $query; + + return $this; + } + + public function rewriteColumn($column, ?string $relation = null): ?AliasedExpression + { + if (! $this->isSelectableColumn($column)) { + return null; + } + + $path = 'from.dependency_node'; + $subQueryRelation = $relation !== null ? $relation . $path : $path; + $subQuery = $this->query->createSubQuery(new DependencyEdge(), $subQueryRelation) + ->limit(1) + ->columns([new Expression('1')]); + + $subQuery->getSelectBase()->join( + ['root_dependency' => 'dependency'], + [$subQuery->getResolver()->getAlias($subQuery->getModel()) . '.dependency_id = root_dependency.id'] + )->join( + ['root_dependency_state' => 'dependency_state'], + ['root_dependency.id = root_dependency_state.dependency_id'] + )->where(new Expression("root_dependency_state.failed = 'y'")); + + $column = $relation !== null ? str_replace('.', '_', $relation) . "_$column" : $column; + + $alias = $this->query->getDb()->quoteIdentifier([$column]); + + list($select, $values) = $this->query->getDb() + ->getQueryBuilder() + ->assembleSelect($subQuery->assembleSelect()); + + return new AliasedExpression($alias, "($select)", null, ...$values); + } + + public function isSelectableColumn(string $name): bool + { + return $name === 'has_root_problem'; + } + + public function rewriteColumnDefinition(ColumnDefinition $def, string $relation): void + { + } + + public function rewriteCondition(Filter\Condition $condition, $relation = null): void + { + $column = substr($condition->getColumn(), strlen($relation)); + + if ($this->isSelectableColumn($column)) { + throw new InvalidColumnException($column, $this->query->getModel()); + } + } +} diff --git a/library/Icingadb/Model/Service.php b/library/Icingadb/Model/Service.php index dea47a75d..d34fd14a2 100644 --- a/library/Icingadb/Model/Service.php +++ b/library/Icingadb/Model/Service.php @@ -6,6 +6,7 @@ use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Model\Behavior\BoolCast; +use Icinga\Module\Icingadb\Model\Behavior\HasRootProblem; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; @@ -194,6 +195,8 @@ public function createBehaviors(Behaviors $behaviors) 'zone_id', 'command_endpoint_id' ])); + + $behaviors->add(new HasRootProblem()); } public function createDefaults(Defaults $defaults) diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 285d374f0..505fdb054 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -21,6 +21,7 @@ use Icinga\Module\Icingadb\Common\Macros; use Icinga\Module\Icingadb\Compat\CompatHost; use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Web\Navigation\Action; use Icinga\Module\Icingadb\Widget\ItemList\DependencyNodeList; @@ -606,7 +607,10 @@ protected function fetchCustomVars() protected function createRootProblems(): ?array { - if ($this->object->state->is_reachable) { + if ( + $this->object->state->is_reachable + || ($this->object instanceof Service && ! $this->object->has_root_problem) + ) { return null; } From 1cb458638d220d56d2e0210c8a78b05df2d7275d Mon Sep 17 00:00:00 2001 From: raviks789 Date: Tue, 1 Oct 2024 14:57:07 +0200 Subject: [PATCH 03/13] RedundancyGroupParentStateSummary: Group by all non-aggregrate columns for postgres --- .../RedundancyGroupParentStateSummary.php | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php index 9f9b58d70..6cf4cf3a1 100644 --- a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php +++ b/library/Icingadb/Model/RedundancyGroupParentStateSummary.php @@ -5,8 +5,10 @@ namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Query; +use ipl\Sql\Adapter\Pgsql; use ipl\Sql\Connection; use ipl\Sql\Expression; +use ipl\Sql\Select; /** * Redundancy group's parent nodes summary @@ -30,7 +32,7 @@ public function getSummaryColumns(): array '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' + . ' + 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)' ), @@ -44,7 +46,7 @@ public function getSummaryColumns(): array ), 'objects_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)' + . ' + CASE WHEN redundancy_group_from_to_service_state.soft_state = 99 THEN 1 ELSE 0 END)' ), 'objects_total' => new Expression( 'SUM(CASE WHEN redundancy_group_from_to_host.id IS NOT NULL THEN 1 ELSE 0 END)' @@ -52,7 +54,7 @@ public function getSummaryColumns(): array ), 'objects_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)' + . ' + CASE WHEN redundancy_group_from_to_service_state.soft_state = 0 THEN 1 ELSE 0 END)' ), 'objects_unknown_handled' => new Expression( 'SUM(CASE WHEN redundancy_group_from_to_service_state.soft_state = 3' @@ -91,6 +93,26 @@ public static function on(Connection $db): Query $m = $q->getModel(); $q->columns($m->getSummaryColumns()); + $q->on($q::ON_SELECT_ASSEMBLED, function (Select $select) use ($q) { + $model = $q->getModel(); + + $groupBy = $q->getResolver()->qualifyColumnsAndAliases((array) $model->getKeyName(), $model, false); + + // For PostgreSQL, ALL non-aggregate SELECT columns must appear in the GROUP BY clause: + if ($q->getDb()->getAdapter() instanceof Pgsql) { + /** + * Ignore Expressions, i.e. aggregate functions {@see getColumns()}, + * which do not need to be added to the GROUP BY. + */ + $candidates = array_filter($select->getColumns(), 'is_string'); + // Remove already considered columns for the GROUP BY, i.e. the primary key. + $candidates = array_diff_assoc($candidates, $groupBy); + $groupBy = array_merge($groupBy, $candidates); + } + + $select->groupBy($groupBy); + }); + return $q; } From 6b4d06dd29a31b92faa3b22f11706fd08ba11751 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Wed, 2 Oct 2024 14:45:38 +0200 Subject: [PATCH 04/13] RedundancyGroupState: change state texts to 'unreachable' or 'reachable' The state text is only necessary to add css rules for state-balls and not be used to show the state text of the group. --- library/Icingadb/Model/RedundancyGroupState.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/Icingadb/Model/RedundancyGroupState.php b/library/Icingadb/Model/RedundancyGroupState.php index d6507d8e2..df30f581e 100644 --- a/library/Icingadb/Model/RedundancyGroupState.php +++ b/library/Icingadb/Model/RedundancyGroupState.php @@ -65,6 +65,7 @@ public function createRelations(Relations $relations): void public function getStateText(): string { - return $this->failed ? 'problem' : 'ok'; + // The method should only be called to fake state balls and not to show the group's state + return $this->failed ? 'unreachable' : 'reachable'; } } From b53e219771a28874384ceac9eeb59003e3b8c786 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Thu, 10 Oct 2024 11:16:45 +0200 Subject: [PATCH 05/13] fix: review comments --- ...eSummary.php => DependencyNodeSummary.php} | 88 +++++++++++-------- ...dges.php => DependencyNodeStateBadges.php} | 10 +-- .../Widget/DependencyNodeStatistics.php | 51 +++++++++++ .../Icingadb/Widget/Detail/ObjectDetail.php | 5 +- .../Widget/ItemList/DependencyNodeList.php | 2 +- .../ItemList/RedundancyGroupListItem.php | 61 +++++++------ library/Icingadb/Widget/ObjectsStatistics.php | 56 ------------ public/css/common.less | 2 +- .../css/list/redundancy-group-list-item.less | 5 +- public/css/widget/nodes-state-badges.less | 3 + public/css/widget/objects-state-badges.less | 3 - 11 files changed, 149 insertions(+), 137 deletions(-) rename library/Icingadb/Model/{RedundancyGroupParentStateSummary.php => DependencyNodeSummary.php} (53%) rename library/Icingadb/Widget/{ObjectsStateBadges.php => DependencyNodeStateBadges.php} (73%) create mode 100644 library/Icingadb/Widget/DependencyNodeStatistics.php delete mode 100644 library/Icingadb/Widget/ObjectsStatistics.php create mode 100644 public/css/widget/nodes-state-badges.less delete mode 100644 public/css/widget/objects-state-badges.less diff --git a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php b/library/Icingadb/Model/DependencyNodeSummary.php similarity index 53% rename from library/Icingadb/Model/RedundancyGroupParentStateSummary.php rename to library/Icingadb/Model/DependencyNodeSummary.php index 6cf4cf3a1..6fb4b1f9a 100644 --- a/library/Icingadb/Model/RedundancyGroupParentStateSummary.php +++ b/library/Icingadb/Model/DependencyNodeSummary.php @@ -13,65 +13,81 @@ /** * Redundancy group's parent nodes summary * - * @property int $objects_problem_handled - * @property int $objects_problem_unhandled - * @property int $objects_pending - * @property int $objects_total - * @property int $objects_ok - * @property int $objects_unknown_handled - * @property int $objects_unknown_unhandled - * @property int $objects_warning_handled - * @property int $objects_warning_unhandled + * @property int $nodes_problem_handled + * @property int $nodes_problem_unhandled + * @property int $nodes_pending + * @property int $nodes_total + * @property int $nodes_ok + * @property int $nodes_unknown_handled + * @property int $nodes_unknown_unhandled + * @property int $nodes_warning_handled + * @property int $nodes_warning_unhandled */ -class RedundancyGroupParentStateSummary extends RedundancyGroup +class DependencyNodeSummary extends RedundancyGroup { public function getSummaryColumns(): array { return [ - 'objects_problem_handled' => 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' + 'nodes_problem_handled' => new Expression( + 'SUM(CASE WHEN' + . ' (redundancy_group_from_to_service.id IS NOT NULL' + . ' AND 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)' + . ' OR redundancy_group_from_to_service_state.is_reachable = \'n\'))' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' + . ' AND 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)' ), - 'objects_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)' + 'nodes_problem_unhandled' => new Expression( + 'SUM(CASE WHEN' + . ' (redundancy_group_from_to_service.id IS NOT NULL' + . ' AND 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\'))' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' + . ' AND 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)' ), - 'objects_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)' + 'nodes_pending' => new Expression( + 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' + . ' AND redundancy_group_from_to_service_state.soft_state = 99)' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' + . ' AND redundancy_group_from_to_host_state.soft_state = 99) THEN 1 ELSE 0 END)' ), - 'objects_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)' + 'nodes_total' => new Expression( + 'SUM(CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL)' + . ' THEN 1 ELSE 0 END)' ), - 'objects_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)' + 'nodes_ok' => new Expression( + 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' + . ' AND redundancy_group_from_to_service_state.soft_state = 0)' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' + . ' AND redundancy_group_from_to_host_state.soft_state = 0) THEN 1 ELSE 0 END)' ), - 'objects_unknown_handled' => new Expression( + 'nodes_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)' ), - 'objects_unknown_unhandled' => new Expression( + 'nodes_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)' ), - 'objects_warning_handled' => new Expression( + 'nodes_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)' ), - 'objects_warning_unhandled' => new Expression( + 'nodes_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)' diff --git a/library/Icingadb/Widget/ObjectsStateBadges.php b/library/Icingadb/Widget/DependencyNodeStateBadges.php similarity index 73% rename from library/Icingadb/Widget/ObjectsStateBadges.php rename to library/Icingadb/Widget/DependencyNodeStateBadges.php index 0a751ad4b..5f6005422 100644 --- a/library/Icingadb/Widget/ObjectsStateBadges.php +++ b/library/Icingadb/Widget/DependencyNodeStateBadges.php @@ -7,23 +7,23 @@ use Icinga\Module\Icingadb\Common\StateBadges; /** - * State badges for the objects + * State badges for the dependency nodes */ -class ObjectsStateBadges extends StateBadges +class DependencyNodeStateBadges extends StateBadges { protected function getType(): string { - return 'objects'; + return 'nodes'; } protected function getPrefix(): string { - return 'objects'; + return 'nodes'; } protected function assemble(): void { - $this->addAttributes(['class' => 'objects-state-badges']); + $this->addAttributes(['class' => 'nodes-state-badges']); $this->add(array_filter([ $this->createGroup('problem'), diff --git a/library/Icingadb/Widget/DependencyNodeStatistics.php b/library/Icingadb/Widget/DependencyNodeStatistics.php new file mode 100644 index 000000000..ffc4403c2 --- /dev/null +++ b/library/Icingadb/Widget/DependencyNodeStatistics.php @@ -0,0 +1,51 @@ +summary = $summary; + } + + protected function createDonut(): ValidHtml + { + $donut = (new Donut()) + ->addSlice($this->summary->nodes_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->nodes_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->nodes_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->nodes_problem_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->nodes_problem_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->nodes_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->nodes_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->nodes_pending, ['class' => 'slice-state-pending']); + + return HtmlString::create($donut->render()); + } + + protected function createTotal(): ValidHtml + { + return Text::create($this->shortenAmount($this->summary->nodes_total)); + } + + protected function createBadges(): ValidHtml + { + return new DependencyNodeStateBadges($this->summary); + } +} diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 505fdb054..a74c79533 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -444,7 +444,7 @@ protected function createPluginOutput(): array 'div', [ 'id' => 'check-output-' . $this->object->checkcommand_name, - 'class' => ['collapsible', 'check-command-output'], + 'class' => 'collapsible', 'data-visible-height' => 100 ], $pluginOutput @@ -607,6 +607,9 @@ protected function fetchCustomVars() protected function createRootProblems(): ?array { + // If a dependency has failed, then the children are not reachable. Hence, the root problems should not be shown + // if the object is not reachable. And in case of a service, since, it may be also be unreachable because of its + // host being down, only show its root problems if they exist. if ( $this->object->state->is_reachable || ($this->object instanceof Service && ! $this->object->has_root_problem) diff --git a/library/Icingadb/Widget/ItemList/DependencyNodeList.php b/library/Icingadb/Widget/ItemList/DependencyNodeList.php index 462e0a3ac..04dfc6ffa 100644 --- a/library/Icingadb/Widget/ItemList/DependencyNodeList.php +++ b/library/Icingadb/Widget/ItemList/DependencyNodeList.php @@ -13,7 +13,7 @@ */ class DependencyNodeList extends StateList { - protected $defaultAttributes = ['class' => ['root-problem-list']]; + protected $defaultAttributes = ['class' => ['dependency-node-list']]; protected function init(): void { diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index cf162f24a..f075ec707 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -8,12 +8,13 @@ use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\ListItemCommonLayout; use Icinga\Module\Icingadb\Model\RedundancyGroup; -use Icinga\Module\Icingadb\Model\RedundancyGroupParentStateSummary; +use Icinga\Module\Icingadb\Model\DependencyNodeSummary; use Icinga\Module\Icingadb\Model\RedundancyGroupState; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; -use Icinga\Module\Icingadb\Widget\ObjectsStatistics; +use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics; use ipl\Html\BaseHtmlElement; +use ipl\I18n\Translation; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Web\Widget\StateBall; @@ -32,10 +33,11 @@ class RedundancyGroupListItem extends StateListItem use ListItemCommonLayout; use Auth; use Database; + use Translation; protected $baseAttributes = ['class' => ['list-item', 'redundancy-group-list-item']]; - /** @var RedundancyGroupParentStateSummary Objects state summary */ + /** @var DependencyNodeSummary Objects state summary */ protected $summary; /** @var RedundancyGroupState */ @@ -48,22 +50,15 @@ protected function init(): void { parent::init(); - $this->summary = RedundancyGroupParentStateSummary::on($this->getDb()) - ->with([ - 'from', - 'from.to.host', - 'from.to.host.state', - 'from.to.service', - 'from.to.service.state' - ]) + $this->summary = DependencyNodeSummary::on($this->getDb()) ->filter(Filter::equal('id', $this->item->id)) ->first(); $this->isHandled = $this->state->failed && ( - $this->summary->objects_problem_handled - || $this->summary->objects_unknown_handled - || $this->summary->objects_warning_handled + $this->summary->nodes_problem_handled + || $this->summary->nodes_unknown_handled + || $this->summary->nodes_warning_handled ); } @@ -101,28 +96,28 @@ protected function assembleCaption(BaseHtmlElement $caption): void $members = RedundancyGroup::on($this->getDb()) ->columns([ 'id' => 'id', - 'objects_output' => new Expression( - 'CASE WHEN redundancy_group_from_to_host_state.output IS NULL' + 'nodes_output' => new Expression( + 'CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' . ' THEN redundancy_group_from_to_service_state.output' . ' ELSE redundancy_group_from_to_host_state.output END' ), - 'objects_long_output' => new Expression( - 'CASE WHEN redundancy_group_from_to_host_state.long_output IS NULL' + 'nodes_long_output' => new Expression( + 'CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' . ' THEN redundancy_group_from_to_service_state.long_output' . ' ELSE redundancy_group_from_to_host_state.long_output END' ), - 'objects_checkcommand_name' => new Expression( - 'CASE WHEN redundancy_group_from_to_host.checkcommand_name IS NULL' + 'nodes_checkcommand_name' => new Expression( + 'CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' . ' THEN redundancy_group_from_to_service.checkcommand_name' . ' ELSE redundancy_group_from_to_host.checkcommand_name END' ), - 'objects_last_state_change' => new Expression( - 'CASE WHEN redundancy_group_from_to_host_state.last_state_change IS NULL' + 'nodes_last_state_change' => new Expression( + 'CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' . ' THEN redundancy_group_from_to_service_state.last_state_change' . ' ELSE redundancy_group_from_to_host_state.last_state_change END' ), - 'objects_severity' => new Expression( - 'CASE WHEN redundancy_group_from_to_host_state.severity IS NULL' + 'nodes_severity' => new Expression( + 'CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' . ' THEN redundancy_group_from_to_service_state.severity' . ' ELSE redundancy_group_from_to_host_state.severity END' ) @@ -136,8 +131,8 @@ protected function assembleCaption(BaseHtmlElement $caption): void ]) ->filter(Filter::equal('id', $this->item->id)) ->orderBy([ - 'objects_severity', - 'objects_last_state_change', + 'nodes_severity', + 'nodes_last_state_change', ], SORT_DESC); $this->applyRestrictions($members); @@ -147,21 +142,25 @@ protected function assembleCaption(BaseHtmlElement $caption): void if ($data) { $caption->addHtml(new PluginOutputContainer( - (new PluginOutput($data->objects_output . "\n" . $data->objects_long_output)) - ->setCommandName($data->objects_checkcommand_name) + (new PluginOutput($data->nodes_output . "\n" . $data->nodes_long_output)) + ->setCommandName($data->nodes_checkcommand_name) )); } - $caption->addHtml(new ObjectsStatistics($this->summary)); + $caption->addHtml(new DependencyNodeStatistics($this->summary)); } protected function assembleTitle(BaseHtmlElement $title): void { $title->addHtml($this->createSubject()); if ($this->state->failed) { - $title->addHtml(HtmlElement::create('span', null, Text::create(t('has no working objects')))); + $title->addHtml(HtmlElement::create( + 'span', + null, + Text::create($this->translate('has no working objects')) + )); } else { - $title->addHtml(HtmlElement::create('span', null, Text::create(t('has working objects')))); + $title->addHtml(HtmlElement::create('span', null, Text::create($this->translate('has working objects')))); } } diff --git a/library/Icingadb/Widget/ObjectsStatistics.php b/library/Icingadb/Widget/ObjectsStatistics.php deleted file mode 100644 index d32eb4b4b..000000000 --- a/library/Icingadb/Widget/ObjectsStatistics.php +++ /dev/null @@ -1,56 +0,0 @@ -summary = $summary; - } - - protected function createDonut(): ValidHtml - { - $donut = (new Donut()) - ->addSlice($this->summary->objects_ok, ['class' => 'slice-state-ok']) - ->addSlice($this->summary->objects_warning_handled, ['class' => 'slice-state-warning-handled']) - ->addSlice($this->summary->objects_warning_unhandled, ['class' => 'slice-state-warning']) - ->addSlice($this->summary->objects_problem_handled, ['class' => 'slice-state-critical-handled']) - ->addSlice($this->summary->objects_problem_unhandled, ['class' => 'slice-state-critical']) - ->addSlice($this->summary->objects_unknown_handled, ['class' => 'slice-state-unknown-handled']) - ->addSlice($this->summary->objects_unknown_unhandled, ['class' => 'slice-state-unknown']) - ->addSlice($this->summary->objects_pending, ['class' => 'slice-state-pending']); - - return HtmlString::create($donut->render()); - } - - protected function createTotal(): ValidHtml - { - return Text::create($this->shortenAmount($this->summary->objects_total)); - } - - protected function createBadges(): ValidHtml - { - $badges = new ObjectsStateBadges($this->summary); - if ($this->hasBaseFilter()) { - $badges->setBaseFilter($this->getBaseFilter()); - } - - return $badges; - } -} diff --git a/public/css/common.less b/public/css/common.less index ee9d1e50f..be596dd74 100644 --- a/public/css/common.less +++ b/public/css/common.less @@ -197,7 +197,7 @@ div.show-more { margin-left: 1em / 1.333em; // 1em / h2 font size } -.object-detail .check-command-output .plugin-output { +.object-detail :not(.caption) > .plugin-output { .rounded-corners(.25em); background-color: @gray-lighter; padding: .5em; diff --git a/public/css/list/redundancy-group-list-item.less b/public/css/list/redundancy-group-list-item.less index 431b44717..7826323da 100644 --- a/public/css/list/redundancy-group-list-item.less +++ b/public/css/list/redundancy-group-list-item.less @@ -1,11 +1,10 @@ -.redundancy-group-list-item.list-item { +.redundancy-group-list-item { .caption { display: flex; justify-content: space-between; .plugin-output { - vertical-align: middle; - .text-ellipsis(); + .line-clamp(2); } .object-statistics { diff --git a/public/css/widget/nodes-state-badges.less b/public/css/widget/nodes-state-badges.less new file mode 100644 index 000000000..4a0e16202 --- /dev/null +++ b/public/css/widget/nodes-state-badges.less @@ -0,0 +1,3 @@ +.nodes-state-badges { + .state-badges(); +} diff --git a/public/css/widget/objects-state-badges.less b/public/css/widget/objects-state-badges.less deleted file mode 100644 index ad369c66a..000000000 --- a/public/css/widget/objects-state-badges.less +++ /dev/null @@ -1,3 +0,0 @@ -.objects-state-badges { - .state-badges(); -} From 928de0b9d83144c4f67c189dd6c5d37f12062a96 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Fri, 11 Oct 2024 10:49:16 +0200 Subject: [PATCH 06/13] fix: HasRootProblem behavior --- .../Model/Behavior/HasRootProblem.php | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/library/Icingadb/Model/Behavior/HasRootProblem.php b/library/Icingadb/Model/Behavior/HasRootProblem.php index 71d0d09a9..76e644e71 100644 --- a/library/Icingadb/Model/Behavior/HasRootProblem.php +++ b/library/Icingadb/Model/Behavior/HasRootProblem.php @@ -35,19 +35,33 @@ public function rewriteColumn($column, ?string $relation = null): ?AliasedExpres return null; } - $path = 'from.dependency_node'; + $path = 'from.'; $subQueryRelation = $relation !== null ? $relation . $path : $path; - $subQuery = $this->query->createSubQuery(new DependencyEdge(), $subQueryRelation) + $subQuery = $this->query->createSubQuery(new DependencyEdge(), $subQueryRelation, null, false) ->limit(1) ->columns([new Expression('1')]); - $subQuery->getSelectBase()->join( - ['root_dependency' => 'dependency'], - [$subQuery->getResolver()->getAlias($subQuery->getModel()) . '.dependency_id = root_dependency.id'] - )->join( - ['root_dependency_state' => 'dependency_state'], - ['root_dependency.id = root_dependency_state.dependency_id'] - )->where(new Expression("root_dependency_state.failed = 'y'")); + $subQueryAlias = $subQuery->getResolver()->getAlias($subQuery->getModel()); + + $subQuery->getSelectBase() + ->join( + ['to_dependency_node' => 'dependency_node'], + ["to_dependency_node.id = $subQueryAlias.to_node_id"] + )->joinLeft( + ['root_dependency' => 'dependency'], + [ "$subQueryAlias.dependency_id = root_dependency.id"] + )->joinLeft( + ['root_dependency_state' => 'dependency_state'], + ['root_dependency.id = root_dependency_state.dependency_id'] + )->joinLeft( + ['root_group' => 'redundancy_group'], + ['root_group.id = to_dependency_node.redundancy_group_id'] + )->joinLeft( + ['root_group_state' => 'redundancy_group_state'], + ['root_group_state.redundancy_group_id = root_group.id'] + )->where( + new Expression("root_dependency_state.failed = 'y' OR root_group_state.failed = 'y'") + )->where($subQueryAlias . '_from.service_id = service.id'); $column = $relation !== null ? str_replace('.', '_', $relation) . "_$column" : $column; From cfe52920860600a7e50b0edc15f27e84a916c93e Mon Sep 17 00:00:00 2001 From: raviks789 Date: Mon, 14 Oct 2024 12:03:19 +0200 Subject: [PATCH 07/13] fix: resolve sd's review comments --- library/Icingadb/Widget/DependencyNodeStateBadges.php | 2 +- library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php | 4 ++-- public/css/widget/dependency-node-state-badges.less | 3 +++ public/css/widget/nodes-state-badges.less | 3 --- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 public/css/widget/dependency-node-state-badges.less delete mode 100644 public/css/widget/nodes-state-badges.less diff --git a/library/Icingadb/Widget/DependencyNodeStateBadges.php b/library/Icingadb/Widget/DependencyNodeStateBadges.php index 5f6005422..6415257d1 100644 --- a/library/Icingadb/Widget/DependencyNodeStateBadges.php +++ b/library/Icingadb/Widget/DependencyNodeStateBadges.php @@ -23,7 +23,7 @@ protected function getPrefix(): string protected function assemble(): void { - $this->addAttributes(['class' => 'nodes-state-badges']); + $this->addAttributes(['class' => 'dependency-node-state-badges']); $this->add(array_filter([ $this->createGroup('problem'), diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index f075ec707..c9519baf9 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -35,7 +35,7 @@ class RedundancyGroupListItem extends StateListItem use Database; use Translation; - protected $baseAttributes = ['class' => ['list-item', 'redundancy-group-list-item']]; + protected $defaultAttributes = ['class' => ['list-item', 'redundancy-group-list-item']]; /** @var DependencyNodeSummary Objects state summary */ protected $summary; @@ -67,7 +67,7 @@ protected function getStateBallSize(): string return StateBall::SIZE_LARGE; } - protected function createTimestamp(): ?BaseHtmlElement + protected function createTimestamp(): BaseHtmlElement { return new TimeSince($this->state->last_state_change->getTimestamp()); } diff --git a/public/css/widget/dependency-node-state-badges.less b/public/css/widget/dependency-node-state-badges.less new file mode 100644 index 000000000..0a2e71e64 --- /dev/null +++ b/public/css/widget/dependency-node-state-badges.less @@ -0,0 +1,3 @@ +.dependency-node-state-badges { + .state-badges(); +} diff --git a/public/css/widget/nodes-state-badges.less b/public/css/widget/nodes-state-badges.less deleted file mode 100644 index 4a0e16202..000000000 --- a/public/css/widget/nodes-state-badges.less +++ /dev/null @@ -1,3 +0,0 @@ -.nodes-state-badges { - .state-badges(); -} From 56fea6b3ef5162949b084d7af9fe428daba5a05c Mon Sep 17 00:00:00 2001 From: raviks789 Date: Mon, 14 Oct 2024 12:27:06 +0200 Subject: [PATCH 08/13] StateListItem: Use `Translator::translatePlural()` to translate affected objects tooltip --- library/Icingadb/Widget/ItemList/StateListItem.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/Icingadb/Widget/ItemList/StateListItem.php b/library/Icingadb/Widget/ItemList/StateListItem.php index 18e73dae4..dacb06922 100644 --- a/library/Icingadb/Widget/ItemList/StateListItem.php +++ b/library/Icingadb/Widget/ItemList/StateListItem.php @@ -96,10 +96,13 @@ protected function assembleTitle(BaseHtmlElement $title): void )); if ($this->state->affects_children) { - $total = $this->item->affected_children; + $total = (int) $this->item->affected_children; - if ((int) $total > 1000) { + if ($total > 1000) { $total = '1000+'; + $tooltip = t('Up to 1000+ affected objects'); + } else { + $tooltip = sprintf(tp('%d affected object', 'Up to %d affected objects', $total), $total); } $icon = new Icon(Icons::UNREACHABLE); @@ -108,7 +111,7 @@ protected function assembleTitle(BaseHtmlElement $title): void 'span', Attributes::create([ 'class' => 'affected-objects', - 'title' => sprintf(t('Up to %s affected objects'), $total) + 'title' => $tooltip ]), $icon, Text::create($total) From 2c460d20b91903e15162684a738adf8cbd5c0c96 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Mon, 14 Oct 2024 12:32:28 +0200 Subject: [PATCH 09/13] Insert trait `Translation` in parent class `StateListItem` instead of `RedundancyGroupListItem` --- .../ItemList/RedundancyGroupListItem.php | 2 -- .../Widget/ItemList/StateListItem.php | 22 ++++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index c9519baf9..302be9df4 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -14,7 +14,6 @@ use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics; use ipl\Html\BaseHtmlElement; -use ipl\I18n\Translation; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Web\Widget\StateBall; @@ -33,7 +32,6 @@ class RedundancyGroupListItem extends StateListItem use ListItemCommonLayout; use Auth; use Database; - use Translation; protected $defaultAttributes = ['class' => ['list-item', 'redundancy-group-list-item']]; diff --git a/library/Icingadb/Widget/ItemList/StateListItem.php b/library/Icingadb/Widget/ItemList/StateListItem.php index dacb06922..f8704d8a6 100644 --- a/library/Icingadb/Widget/ItemList/StateListItem.php +++ b/library/Icingadb/Widget/ItemList/StateListItem.php @@ -12,6 +12,7 @@ use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use ipl\Html\Attributes; use ipl\Html\HtmlElement; +use ipl\I18n\Translation; use ipl\Web\Common\BaseListItem; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\TimeSince; @@ -26,6 +27,8 @@ */ abstract class StateListItem extends BaseListItem { + use Translation; + /** @var StateList The list where the item is part of */ protected $list; @@ -66,10 +69,10 @@ protected function createIconImage(): ?BaseHtmlElement protected function assembleCaption(BaseHtmlElement $caption): void { if ($this->state->soft_state === null && $this->state->output === null) { - $caption->addHtml(Text::create(t('Waiting for Icinga DB to synchronize the state.'))); + $caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.'))); } else { if (empty($this->state->output)) { - $pluginOutput = new EmptyState(t('Output unavailable.')); + $pluginOutput = new EmptyState($this->translate('Output unavailable.')); } else { $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item)); } @@ -90,7 +93,7 @@ protected function assembleIconImage(BaseHtmlElement $iconImage): void protected function assembleTitle(BaseHtmlElement $title): void { $title->addHtml(Html::sprintf( - t('%s is %s', ' is '), + $this->translate('%s is %s', ' is '), $this->createSubject(), Html::tag('span', ['class' => 'state-text'], $this->state->getStateTextTranslated()) )); @@ -100,9 +103,16 @@ protected function assembleTitle(BaseHtmlElement $title): void if ($total > 1000) { $total = '1000+'; - $tooltip = t('Up to 1000+ affected objects'); + $tooltip = $this->translate('Up to 1000+ affected objects'); } else { - $tooltip = sprintf(tp('%d affected object', 'Up to %d affected objects', $total), $total); + $tooltip = sprintf( + $this->translatePlural( + '%d affected object', + 'Up to %d affected objects', + $total + ), + $total + ); } $icon = new Icon(Icons::UNREACHABLE); @@ -140,7 +150,7 @@ protected function createTimestamp(): ?BaseHtmlElement $since = null; if ($this->state->is_overdue) { $since = new TimeSince($this->state->next_update->getTimestamp()); - $since->prepend(t('Overdue') . ' '); + $since->prepend($this->translate('Overdue') . ' '); $since->prependHtml(new Icon(Icons::WARNING)); } elseif ($this->state->last_state_change !== null && $this->state->last_state_change->getTimestamp() > 0) { $since = new TimeSince($this->state->last_state_change->getTimestamp()); From 87209782491d2731f740fa6f282b65b9fb67c38d Mon Sep 17 00:00:00 2001 From: raviks789 Date: Mon, 14 Oct 2024 16:37:48 +0200 Subject: [PATCH 10/13] Change name `HasRootProblem` to `HasProblematicParent` --- application/controllers/ServiceController.php | 2 +- .../{HasRootProblem.php => HasProblematicParent.php} | 6 +++--- library/Icingadb/Model/Service.php | 4 ++-- library/Icingadb/Widget/Detail/ObjectDetail.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename library/Icingadb/Model/Behavior/{HasRootProblem.php => HasProblematicParent.php} (94%) diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index 90b3a4412..2857718ae 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -39,7 +39,7 @@ public function init() $hostName = $this->params->getRequired('host.name'); $query = Service::on($this->getDb()) - ->withColumns(['has_root_problem']) + ->withColumns(['has_problematic_parent']) ->with([ 'state', 'icon_image', diff --git a/library/Icingadb/Model/Behavior/HasRootProblem.php b/library/Icingadb/Model/Behavior/HasProblematicParent.php similarity index 94% rename from library/Icingadb/Model/Behavior/HasRootProblem.php rename to library/Icingadb/Model/Behavior/HasProblematicParent.php index 76e644e71..99ee00800 100644 --- a/library/Icingadb/Model/Behavior/HasRootProblem.php +++ b/library/Icingadb/Model/Behavior/HasProblematicParent.php @@ -15,9 +15,9 @@ use ipl\Orm\Contract\QueryAwareBehavior; /** - * Behavior to check if the object has a root problem + * Behavior to check if the service has a problematic parent */ -class HasRootProblem implements RewriteColumnBehavior, QueryAwareBehavior +class HasProblematicParent implements RewriteColumnBehavior, QueryAwareBehavior { /** @var Query */ protected $query; @@ -76,7 +76,7 @@ public function rewriteColumn($column, ?string $relation = null): ?AliasedExpres public function isSelectableColumn(string $name): bool { - return $name === 'has_root_problem'; + return $name === 'has_problematic_parent'; } public function rewriteColumnDefinition(ColumnDefinition $def, string $relation): void diff --git a/library/Icingadb/Model/Service.php b/library/Icingadb/Model/Service.php index d34fd14a2..d895789ec 100644 --- a/library/Icingadb/Model/Service.php +++ b/library/Icingadb/Model/Service.php @@ -6,7 +6,7 @@ use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Model\Behavior\BoolCast; -use Icinga\Module\Icingadb\Model\Behavior\HasRootProblem; +use Icinga\Module\Icingadb\Model\Behavior\HasProblematicParent; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; @@ -196,7 +196,7 @@ public function createBehaviors(Behaviors $behaviors) 'command_endpoint_id' ])); - $behaviors->add(new HasRootProblem()); + $behaviors->add(new HasProblematicParent()); } public function createDefaults(Defaults $defaults) diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index a74c79533..9a95f297d 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -612,7 +612,7 @@ protected function createRootProblems(): ?array // host being down, only show its root problems if they exist. if ( $this->object->state->is_reachable - || ($this->object instanceof Service && ! $this->object->has_root_problem) + || ($this->object instanceof Service && ! $this->object->has_problematic_parent) ) { return null; } From a2327810159970159d9ff427a9570336818059a2 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Tue, 15 Oct 2024 12:06:42 +0200 Subject: [PATCH 11/13] Refactor `DependencyNodeSummary` name to `RedundancyGroupSummary` --- ...Summary.php => RedundancyGroupSummary.php} | 22 +++++++++---------- .../Widget/DependencyNodeStatistics.php | 4 ++-- .../ItemList/RedundancyGroupListItem.php | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) rename library/Icingadb/Model/{DependencyNodeSummary.php => RedundancyGroupSummary.php} (98%) diff --git a/library/Icingadb/Model/DependencyNodeSummary.php b/library/Icingadb/Model/RedundancyGroupSummary.php similarity index 98% rename from library/Icingadb/Model/DependencyNodeSummary.php rename to library/Icingadb/Model/RedundancyGroupSummary.php index 6fb4b1f9a..2b3db7ff3 100644 --- a/library/Icingadb/Model/DependencyNodeSummary.php +++ b/library/Icingadb/Model/RedundancyGroupSummary.php @@ -11,23 +11,30 @@ use ipl\Sql\Select; /** - * Redundancy group's parent nodes summary + * Redundancy group's summary (The nodes could only host and service) * + * @property int $nodes_total + * @property int $nodes_ok * @property int $nodes_problem_handled * @property int $nodes_problem_unhandled * @property int $nodes_pending - * @property int $nodes_total - * @property int $nodes_ok * @property int $nodes_unknown_handled * @property int $nodes_unknown_unhandled * @property int $nodes_warning_handled * @property int $nodes_warning_unhandled */ -class DependencyNodeSummary extends RedundancyGroup +class RedundancyGroupSummary extends RedundancyGroup { public function getSummaryColumns(): array { return [ + 'nodes_ok' => new Expression( + 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' + . ' AND redundancy_group_from_to_service_state.soft_state = 0)' + . ' OR' + . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' + . ' AND redundancy_group_from_to_host_state.soft_state = 0) THEN 1 ELSE 0 END)' + ), 'nodes_problem_handled' => new Expression( 'SUM(CASE WHEN' . ' (redundancy_group_from_to_service.id IS NOT NULL' @@ -65,13 +72,6 @@ public function getSummaryColumns(): array . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL)' . ' THEN 1 ELSE 0 END)' ), - 'nodes_ok' => new Expression( - 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' - . ' AND redundancy_group_from_to_service_state.soft_state = 0)' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' - . ' AND redundancy_group_from_to_host_state.soft_state = 0) THEN 1 ELSE 0 END)' - ), 'nodes_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\'' diff --git a/library/Icingadb/Widget/DependencyNodeStatistics.php b/library/Icingadb/Widget/DependencyNodeStatistics.php index ffc4403c2..53e8d5e23 100644 --- a/library/Icingadb/Widget/DependencyNodeStatistics.php +++ b/library/Icingadb/Widget/DependencyNodeStatistics.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; -use Icinga\Module\Icingadb\Model\DependencyNodeSummary; +use Icinga\Module\Icingadb\Model\RedundancyGroupSummary; use Icinga\Module\Icingadb\Widget\Detail\ObjectStatistics; use ipl\Html\Text; use ipl\Html\ValidHtml; @@ -16,7 +16,7 @@ */ class DependencyNodeStatistics extends ObjectStatistics { - /** @var DependencyNodeSummary Dependency node summary */ + /** @var RedundancyGroupSummary Dependency node summary */ protected $summary; public function __construct($summary) diff --git a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php index 302be9df4..ed4f02b4c 100644 --- a/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php +++ b/library/Icingadb/Widget/ItemList/RedundancyGroupListItem.php @@ -8,7 +8,7 @@ use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\ListItemCommonLayout; use Icinga\Module\Icingadb\Model\RedundancyGroup; -use Icinga\Module\Icingadb\Model\DependencyNodeSummary; +use Icinga\Module\Icingadb\Model\RedundancyGroupSummary; use Icinga\Module\Icingadb\Model\RedundancyGroupState; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; @@ -35,7 +35,7 @@ class RedundancyGroupListItem extends StateListItem protected $defaultAttributes = ['class' => ['list-item', 'redundancy-group-list-item']]; - /** @var DependencyNodeSummary Objects state summary */ + /** @var RedundancyGroupSummary Objects state summary */ protected $summary; /** @var RedundancyGroupState */ @@ -48,7 +48,7 @@ protected function init(): void { parent::init(); - $this->summary = DependencyNodeSummary::on($this->getDb()) + $this->summary = RedundancyGroupSummary::on($this->getDb()) ->filter(Filter::equal('id', $this->item->id)) ->first(); From 061f67a6dc3cd530b7311526eee1b66a44db2592 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Thu, 17 Oct 2024 14:54:34 +0200 Subject: [PATCH 12/13] fix: cleanup `RedundancyGroupSummary` --- .../Icingadb/Model/RedundancyGroupSummary.php | 142 ++++++++++++------ 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/library/Icingadb/Model/RedundancyGroupSummary.php b/library/Icingadb/Model/RedundancyGroupSummary.php index 2b3db7ff3..6a1cc52f0 100644 --- a/library/Icingadb/Model/RedundancyGroupSummary.php +++ b/library/Icingadb/Model/RedundancyGroupSummary.php @@ -28,69 +28,115 @@ class RedundancyGroupSummary extends RedundancyGroup public function getSummaryColumns(): array { return [ + 'nodes_total' => new Expression( + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN 1' + . ' WHEN %s IS NOT NULL THEN 1' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.host_id', + ] + ), 'nodes_ok' => new Expression( - 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' - . ' AND redundancy_group_from_to_service_state.soft_state = 0)' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' - . ' AND redundancy_group_from_to_host_state.soft_state = 0) THEN 1 ELSE 0 END)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 0 THEN 1 ELSE 0 END)' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 0 THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service.id', + 'from.to.service.state.soft_state', + 'from.to.host_id', + 'from.to.host.state.soft_state', + ] ), 'nodes_problem_handled' => new Expression( - 'SUM(CASE WHEN' - . ' (redundancy_group_from_to_service.id IS NOT NULL' - . ' AND 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\'))' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' - . ' AND 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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = \'y\' OR %s = \'n\') THEN 1 ELSE 0 END)' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = \'y\' OR %s = \'n\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable', + 'from.to.host_id', + 'from.to.host.state.soft_state', + 'from.to.host.state.is_handled', + 'from.to.host.state.is_reachable', + ] ), 'nodes_problem_unhandled' => new Expression( - 'SUM(CASE WHEN' - . ' (redundancy_group_from_to_service.id IS NOT NULL' - . ' AND 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\'))' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' - . ' AND 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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = \'n\' AND %s = \'y\') THEN 1 ELSE 0 END)' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = \'n\' AND %s = \'y\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable', + 'from.to.host_id', + 'from.to.host.state.soft_state', + 'from.to.host.state.is_handled', + 'from.to.host.state.is_reachable', + ] ), 'nodes_pending' => new Expression( - 'SUM(CASE WHEN (redundancy_group_from_to_service.id IS NOT NULL' - . ' AND redundancy_group_from_to_service_state.soft_state = 99)' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL' - . ' AND redundancy_group_from_to_host_state.soft_state = 99) THEN 1 ELSE 0 END)' - ), - 'nodes_total' => new Expression( - 'SUM(CASE WHEN redundancy_group_from_to_service.id IS NOT NULL' - . ' OR' - . ' (redundancy_group_from_to_host.id IS NOT NULL AND redundancy_group_from_to_service.id IS NULL)' - . ' THEN 1 ELSE 0 END)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 99 THEN 1 ELSE 0 END)' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 99 THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service.id', + 'from.to.service.state.soft_state', + 'from.to.host_id', + 'from.to.host.state.soft_state', + ] ), 'nodes_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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = \'y\' OR %s = \'n\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable' + ] ), 'nodes_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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = \'n\' AND %s = \'y\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable' + ] ), 'nodes_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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = \'y\' OR %s = \'n\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable' + ] ), 'nodes_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)' + 'SUM(CASE' + . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = \'n\' AND %s = \'y\') THEN 1 ELSE 0 END)' + . ' ELSE 0 END)', + [ + 'from.to.service_id', + 'from.to.service.state.soft_state', + 'from.to.service.state.is_handled', + 'from.to.service.state.is_reachable' + ] ) ]; } From c5625e5e0a2346e9de6e4678a9c5a6d7c6830147 Mon Sep 17 00:00:00 2001 From: raviks789 Date: Fri, 4 Oct 2024 11:43:20 +0200 Subject: [PATCH 13/13] Fake data to indicate affected children --- library/Icingadb/Common/IcingaRedis.php | 12 +++++++++++- library/Icingadb/Model/Host.php | 3 ++- library/Icingadb/Model/Service.php | 3 ++- library/Icingadb/Model/State.php | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/Icingadb/Common/IcingaRedis.php b/library/Icingadb/Common/IcingaRedis.php index a22a0f03b..84264815b 100644 --- a/library/Icingadb/Common/IcingaRedis.php +++ b/library/Icingadb/Common/IcingaRedis.php @@ -8,6 +8,7 @@ use Generator; use Icinga\Application\Config; use Icinga\Application\Logger; +use ipl\Sql\Expression; use Predis\Client as Redis; class IcingaRedis @@ -163,7 +164,16 @@ protected static function fetchState(string $key, array $ids, array $columns): G foreach ($results as $i => $json) { if ($json !== null) { $data = json_decode($json, true); - $keyMap = array_fill_keys($columns, null); + $keyMap = []; + + foreach ($columns as $alias => $column) { + if ($column instanceof Expression) { + $keyMap[$alias] = $column->getStatement() ; + } else { + $keyMap[$alias] = null; + } + } + unset($keyMap['is_overdue']); // Is calculated by Icinga DB, not Icinga 2, hence it's never in redis // TODO: Remove once https://github.com/Icinga/icinga2/issues/9427 is fixed diff --git a/library/Icingadb/Model/Host.php b/library/Icingadb/Model/Host.php index 05c35bc07..57eef2b5a 100644 --- a/library/Icingadb/Model/Host.php +++ b/library/Icingadb/Model/Host.php @@ -13,6 +13,7 @@ use ipl\Orm\Model; use ipl\Orm\Relations; use ipl\Orm\ResultSet; +use ipl\Sql\Expression; /** * Host model. @@ -113,7 +114,7 @@ public function getColumns() 'zone_id', 'command_endpoint_name', 'command_endpoint_id', - 'affected_children' + 'affected_children' => new Expression('200000') ]; } diff --git a/library/Icingadb/Model/Service.php b/library/Icingadb/Model/Service.php index d895789ec..2cf7ab89b 100644 --- a/library/Icingadb/Model/Service.php +++ b/library/Icingadb/Model/Service.php @@ -14,6 +14,7 @@ use ipl\Orm\Model; use ipl\Orm\Relations; use ipl\Orm\ResultSet; +use ipl\Sql\Expression; /** * @property string $id @@ -106,7 +107,7 @@ public function getColumns() 'zone_id', 'command_endpoint_name', 'command_endpoint_id', - 'affected_children' + 'affected_children' => new Expression('10') ]; } diff --git a/library/Icingadb/Model/State.php b/library/Icingadb/Model/State.php index a4556c1d5..fa2eab6e7 100644 --- a/library/Icingadb/Model/State.php +++ b/library/Icingadb/Model/State.php @@ -11,6 +11,7 @@ use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use ipl\Orm\Model; +use ipl\Sql\Expression; use ipl\Web\Widget\Icon; /** @@ -100,7 +101,7 @@ public function getColumns() 'last_state_change', 'next_check', 'next_update', - 'affects_children' + 'affects_children' => new Expression("'y'") ]; }