diff --git a/application/controllers/CommandController.php b/application/controllers/CommandController.php index 0b302af7..92070c65 100644 --- a/application/controllers/CommandController.php +++ b/application/controllers/CommandController.php @@ -2,13 +2,18 @@ namespace Icinga\Module\Director\Controllers; +use dipl\Html\Html; use Icinga\Module\Director\Forms\IcingaCommandArgumentForm; use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Resolver\CommandUsage; use Icinga\Module\Director\Web\Controller\ObjectController; use Icinga\Module\Director\Web\Table\IcingaCommandArgumentTable; class CommandController extends ObjectController { + /** + * @throws \Icinga\Exception\ProgrammingError + */ public function init() { parent::init(); @@ -22,6 +27,56 @@ class CommandController extends ObjectController } } + /** + * @throws \Icinga\Exception\ProgrammingError + */ + public function indexAction() + { + $this->showUsage(); + parent::indexAction(); + } + + /** + * @throws \Icinga\Exception\ProgrammingError + */ + public function renderAction() + { + if ($this->object->isExternal()) { + $this->showUsage(); + } + + parent::renderAction(); + } + + /** + * @throws \Icinga\Exception\ProgrammingError + */ + protected function showUsage() + { + /** @var IcingaCommand $command */ + $command = $this->object; + if ($command->isInUse()) { + $usage = new CommandUsage($command); + $this->content()->add(Html::tag('p', [ + 'class' => 'information', + 'data-base-target' => '_next' + ], Html::sprintf( + $this->translate('This Command is currently being used by %s'), + Html::tag('span', null, $usage->getLinks())->setSeparator(', ') + ))); + } else { + $this->content()->add(Html::tag( + 'p', + ['class' => 'warning'], + $this->translate('This Command is currently not in use') + )); + } + } + + /** + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\ProgrammingError + */ public function argumentsAction() { $p = $this->params; diff --git a/doc/82-Changelog.md b/doc/82-Changelog.md index 7d436c82..150079c0 100644 --- a/doc/82-Changelog.md +++ b/doc/82-Changelog.md @@ -23,6 +23,7 @@ before switching to a new version. * FEATURE: Users equipped with related permissions can toggle "Show SQL" in the GUI * FEATURE: A Service Set can now be assigned to multiple hosts at once #1281 * FEATURE: Commands can now be filtered by usage (#1480) +* FEATURE: Show usage of Commands over templates and objects (#335) * FIX: Don't suggest Command templates where Commands are required (#1414) * FIX: Do not allow to delete Commands being used by other objects (#1443) diff --git a/library/Director/Resolver/CommandUsage.php b/library/Director/Resolver/CommandUsage.php new file mode 100644 index 00000000..cd9f0c83 --- /dev/null +++ b/library/Director/Resolver/CommandUsage.php @@ -0,0 +1,106 @@ +isTemplate()) { + throw new ProgrammingError( + 'CommandUsageTable expects object or external_object, got a template' + ); + } + + $this->command = $command; + $this->db = $command->getDb(); + } + + /** + * @return array + * @throws ProgrammingError + */ + public function getLinks() + { + $name = $this->command->getObjectName(); + $links = []; + $map = [ + 'host' => ['check_command', 'event_command'], + 'service' => ['check_command', 'event_command'], + 'notification' => ['command'], + ]; + $types = [ + 'host' => [ + 'object' => $this->translate('%d Host(s)'), + 'template' => $this->translate('%d Host Template(s)'), + ], + 'service' => [ + 'object' => $this->translate('%d Service(s)'), + 'template' => $this->translate('%d Service Template(s)'), + 'apply' => $this->translate('%d Service Apply Rule(s)'), + ], + 'notification' => [ + 'object' => $this->translate('%d Notification(s)'), + 'template' => $this->translate('%d Notification Template(s)'), + 'apply' => $this->translate('%d Notification Apply Rule(s)'), + ], + ]; + + $urlSuffix = [ + 'object' => '', + 'template' => '/templates', + 'apply' => '/applyrules', + ]; + + foreach ($map as $type => $relations) { + $res = $this->fetchFor($type, $relations, array_keys($types[$type])); + foreach ($types[$type] as $objectType => $caption) { + if ($res->$objectType > 0) { + $suffix = $urlSuffix[$objectType]; + $links[] = Link::create( + sprintf($caption, $res->$objectType), + "director/${type}s$suffix", + ['command' => $name] + ); + } + } + } + + return $links; + } + + protected function fetchFor($table, $rels, $objectTypes) + { + $id = $this->command->getAutoincId(); + + $columns = []; + foreach ($objectTypes as $type) { + $columns[$type] = "COALESCE(SUM(CASE WHEN object_type = '$type' THEN 1 ELSE 0 END), 0)"; + } + $query = $this->db->select()->from("icinga_$table", $columns); + + foreach ($rels as $rel) { + $query->orWhere("${rel}_id = ?", $id); + } + + return $this->db->fetchRow($query); + } +} diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php index e13df9fe..15a5631a 100644 --- a/library/Director/Web/Controller/ObjectsController.php +++ b/library/Director/Web/Controller/ObjectsController.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Web\Controller; +use dipl\Web\Table\ZfQueryBasedTable; use Icinga\Data\Filter\FilterChain; use Icinga\Data\Filter\FilterExpression; use Icinga\Exception\NotFoundError; use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Forms\IcingaMultiEditForm; +use Icinga\Module\Director\Objects\IcingaCommand; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\RestApi\IcingaObjectsHandler; @@ -112,7 +114,8 @@ abstract class ObjectsController extends ActionController } // Hint: might be used in controllers extending this - $this->table = $this->getTable(); + $this->table = $this->eventuallyFilterCommand($this->getTable()); + $this->table->renderTo($this); (new AdditionalTableActions($this->getAuth(), $this->url(), $this->table)) ->appendTo($this->actions()); @@ -187,9 +190,13 @@ abstract class ObjectsController extends ActionController ) ->actions(new TemplateActionBar($shortType, $this->url())); - $this->params->get('render') === 'tree' - ? TemplateTreeRenderer::showType($shortType, $this, $this->db()) - : TemplatesTable::create($shortType, $this->db())->renderTo($this); + if ($this->params->get('render') === 'tree') { + TemplateTreeRenderer::showType($shortType, $this, $this->db()); + } else { + $table = TemplatesTable::create($shortType, $this->db()); + $this->eventuallyFilterCommand($table); + $table->renderTo($this); + } } /** @@ -239,6 +246,7 @@ abstract class ObjectsController extends ActionController $table = new ApplyRulesTable($this->db()); $table->setType($this->getType()); + $this->eventuallyFilterCommand($table); $table->renderTo($this); } @@ -311,6 +319,37 @@ abstract class ObjectsController extends ActionController return $objects; } + /** + * @param ZfQueryBasedTable $table + * @return ZfQueryBasedTable + * @throws \Icinga\Exception\ConfigurationError + */ + protected function eventuallyFilterCommand(ZfQueryBasedTable $table) + { + if ($this->params->get('command')) { + $command = IcingaCommand::load($this->params->get('command'), $this->db()); + switch ($this->getBaseType()) { + case 'host': + case 'service': + $table->getQuery()->where( + $this->db()->getDbAdapter()->quoteInto( + '(o.check_command_id = ? OR o.event_command_id = ?)', + $command->getAutoincId() + ) + ); + break; + case 'notification': + $table->getQuery()->where( + 'o.command_id = ?', + $command->getAutoincId() + ); + break; + } + } + + return $table; + } + /** * @param $feature * @return $this diff --git a/public/css/module.less b/public/css/module.less index edb52bee..5c36a9b8 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -1041,6 +1041,18 @@ p.warning { } } +p.information { + color: white; + padding: 1em 2em; + background-color: @colorOk; + font-weight: bold; + + a { + color: inherit; + text-decoration: underline; + } +} + table.tinystats { font-size: 0.7em; float: right;