diff --git a/application/controllers/InspectController.php b/application/controllers/InspectController.php index 9aec3da6..d6316523 100644 --- a/application/controllers/InspectController.php +++ b/application/controllers/InspectController.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Controllers; +use gipfl\IcingaWeb2\Link; use Icinga\Module\Director\Objects\IcingaEndpoint; use Icinga\Module\Director\PlainObjectRenderer; use Icinga\Module\Director\Web\Controller\ActionController; @@ -10,9 +11,9 @@ use Icinga\Module\Director\Web\Table\CoreApiObjectsTable; use Icinga\Module\Director\Web\Table\CoreApiPrototypesTable; use Icinga\Module\Director\Web\Tabs\ObjectTabs; use Icinga\Module\Director\Web\Tree\InspectTreeRenderer; -use ipl\Html\Html; -use gipfl\IcingaWeb2\Link; use Icinga\Module\Director\Web\Widget\IcingaObjectInspection; +use Icinga\Module\Director\Web\Widget\InspectPackages; +use ipl\Html\Html; class InspectController extends ActionController { @@ -24,9 +25,7 @@ class InspectController extends ActionController } /** - * @throws \Icinga\Exception\Http\HttpNotFoundException - * @throws \Icinga\Exception\IcingaException - * @throws \Icinga\Exception\ProgrammingError + * @throws \Icinga\Exception\NotFoundError */ public function typesAction() { @@ -55,8 +54,7 @@ class InspectController extends ActionController } /** - * @throws \Icinga\Exception\IcingaException - * @throws \Icinga\Exception\ProgrammingError + * @throws \Icinga\Exception\NotFoundError */ public function typeAction() { @@ -95,9 +93,7 @@ class InspectController extends ActionController } /** - * @throws \Icinga\Exception\ConfigurationError - * @throws \Icinga\Exception\IcingaException - * @throws \Icinga\Exception\ProgrammingError + * @throws \Icinga\Exception\NotFoundError */ public function objectAction() { @@ -116,7 +112,6 @@ class InspectController extends ActionController /** * @param IcingaEndpoint $endpoint - * @throws \Icinga\Exception\IcingaException */ protected function showEndpointInformation(IcingaEndpoint $endpoint) { @@ -132,8 +127,6 @@ class InspectController extends ActionController /** * @param IcingaEndpoint $endpoint * @return Link - * @throws \Icinga\Exception\ProgrammingError - * @throws \Icinga\Exception\IcingaException */ protected function linkToEndpoint(IcingaEndpoint $endpoint) { @@ -143,8 +136,7 @@ class InspectController extends ActionController } /** - * @throws \Icinga\Exception\IcingaException - * @throws \Icinga\Exception\ProgrammingError + * @throws \Icinga\Exception\NotFoundError */ public function statusAction() { @@ -157,10 +149,40 @@ class InspectController extends ActionController )); } + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function packagesAction() + { + $db = $this->db(); + $endpointName = $this->params->get('endpoint'); + $package = $this->params->get('package'); + $stage = $this->params->get('stage'); + $file = $this->params->get('file'); + if ($endpointName === null) { + $endpoint = null; + } else { + $endpoint = IcingaEndpoint::load($endpointName, $db); + } + if ($endpoint === null) { + $this->addSingleTab($this->translate('Inspect Packages')); + } elseif ($file !== null) { + $this->addSingleTab($this->translate('Inspect File Content')); + } else { + $this->tabs( + new ObjectTabs('endpoint', $this->Auth(), $endpoint) + )->activate('packages'); + } + $widget = new InspectPackages($this->db(), 'director/inspect/packages'); + $this->addTitle($widget->getTitle($endpoint, $package, $stage, $file)); + if ($file === null) { + $this->actions()->add($widget->getBreadCrumb($endpoint, $package, $stage)); + } + $this->content()->add($widget->getContent($endpoint, $package, $stage, $file)); + } + /** * @return IcingaEndpoint - * @throws \Icinga\Exception\ConfigurationError - * @throws \Icinga\Exception\IcingaException * @throws \Icinga\Exception\NotFoundError */ protected function endpoint() diff --git a/doc/82-Changelog.md b/doc/82-Changelog.md index 933a59c3..6b6c86b8 100644 --- a/doc/82-Changelog.md +++ b/doc/82-Changelog.md @@ -14,6 +14,7 @@ next (will be 1.8.0) ### User Interface * FIX: It's now possible to set Endpoint ports > 32767 on PostgreSQL (#928) * FEATURE: Data Fields can now be grouped into categories (#1969) +* FEATURE: Inspect is now available for Packages, Stages and Files (#1995) 1.7.2 ----- diff --git a/library/Director/Core/CoreApi.php b/library/Director/Core/CoreApi.php index 5eb39e96..eaed12b6 100644 --- a/library/Director/Core/CoreApi.php +++ b/library/Director/Core/CoreApi.php @@ -708,24 +708,30 @@ constants } } - public function listStageFiles($stage) + public function listStageFiles($stage, $packageName = null) { + if ($packageName === null) { + $packageName = $this->getPackageName(); + } return array_keys( - $this->client()->get(sprintf( + $this->client()->get(\sprintf( 'config/stages/%s/%s', - urlencode($this->getPackageName()), - urlencode($stage) + \urlencode($packageName), + \urlencode($stage) ))->getResult('name', array('type' => 'file')) ); } - public function getStagedFile($stage, $file) + public function getStagedFile($stage, $file, $packageName = null) { - return $this->client()->getRaw(sprintf( + if ($packageName === null) { + $packageName = $this->getPackageName(); + } + return $this->client()->getRaw(\sprintf( 'config/files/%s/%s/%s', - urlencode($this->getPackageName()), - urlencode($stage), - urlencode($file) + \urlencode($packageName), + \urlencode($stage), + \urlencode($file) )); } diff --git a/library/Director/Db.php b/library/Director/Db.php index 436aeded..af859ba0 100644 --- a/library/Director/Db.php +++ b/library/Director/Db.php @@ -192,6 +192,26 @@ class Db extends DbConnection return $db->fetchOne($query) > 0; } + public function getEndpointNamesInDeploymentZone() + { + $db = $this->db(); + $query = $db->select()->from( + array('z' => 'icinga_zone'), + array('object_name' => 'e.object_name') + )->join( + array('e' => 'icinga_endpoint'), + 'e.zone_id = z.id', + array() + )->join( + array('au' => 'icinga_apiuser'), + 'e.apiuser_id = au.id', + array() + )->where('z.object_name = ?', $this->getMasterZoneName()) + ->order('e.object_name ASC'); + + return $db->fetchCol($query) ?: []; + } + public function getDeploymentEndpointName() { $db = $this->db(); diff --git a/library/Director/Web/Tabs/ObjectTabs.php b/library/Director/Web/Tabs/ObjectTabs.php index bfb16e0e..11c5bfd8 100644 --- a/library/Director/Web/Tabs/ObjectTabs.php +++ b/library/Director/Web/Tabs/ObjectTabs.php @@ -131,6 +131,11 @@ class ObjectTabs extends Tabs 'urlParams' => ['endpoint' => $object->getObjectName()], 'label' => $this->translate('Inspect') ]); + $this->add('packages', [ + 'url' => 'director/inspect/packages', + 'urlParams' => ['endpoint' => $object->getObjectName()], + 'label' => $this->translate('Packages') + ]); } if ($object->getShortTableName() === 'host') { diff --git a/library/Director/Web/Widget/InspectPackages.php b/library/Director/Web/Widget/InspectPackages.php new file mode 100644 index 00000000..f9b8864f --- /dev/null +++ b/library/Director/Web/Widget/InspectPackages.php @@ -0,0 +1,174 @@ +db = $db; + $this->baseUrl = $baseUrl; + } + + public function getContent(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null) + { + if ($endpoint === null) { + return $this->getRootEndpoints(); + } elseif ($package === null) { + return $this->getPackages($endpoint); + } elseif ($stage === null) { + return $this->getStages($endpoint, $package); + } elseif ($file === null) { + return $this->getFiles($endpoint, $package, $stage); + } else { + return $this->getFile($endpoint, $package, $stage, $file); + } + } + + public function getTitle(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null) + { + if ($endpoint === null) { + return $this->translate('Endpoint in your Root Zone'); + } elseif ($package === null) { + return \sprintf($this->translate('Packages on Endpoint: %s'), $endpoint->getObjectName()); + } elseif ($stage === null) { + return \sprintf($this->translate('Stages in Package: %s'), $package); + } elseif ($file === null) { + return \sprintf($this->translate('Files in Stage: %s'), $stage); + } else { + return \sprintf($this->translate('File Content: %s'), $file); + } + } + + public function getBreadCrumb(IcingaEndpoint $endpoint = null, $package = null, $stage = null) + { + $parts = [ + 'endpoint' => $endpoint === null ? null : $endpoint->getObjectName(), + 'package' => $package, + 'stage' => $stage, + ]; + + $params = []; + // No root zone link for now: + // $result = [Link::create($this->translate('Root Zone'), $this->baseUrl)]; + $result = [Html::tag('a', ['href' => '#'], $this->translate('Root Zone'))]; + foreach ($parts as $name => $value) { + if ($value === null) { + break; + } + $params[$name] = $value; + $result[] = Link::create($value, $this->baseUrl, $params); + } + + return Html::tag('ul', ['class' => 'breadcrumb'], Html::wrapEach($result, 'li')); + } + + protected function getRootEndpoints() + { + $table = $this->prepareTable(); + foreach ($this->db->getEndpointNamesInDeploymentZone() as $name) { + $table->add(Table::row([ + Link::create($name, $this->baseUrl, [ + 'endpoint' => $name, + ]) + ])); + } + + return $table; + } + + protected function getPackages(IcingaEndpoint $endpoint) + { + $table = $this->prepareTable(); + $api = $endpoint->api(); + foreach ($api->getPackages() as $package) { + $table->add(Table::row([ + Link::create($package->name, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package->name, + ]) + ])); + } + + return $table; + } + + protected function getStages(IcingaEndpoint $endpoint, $packageName) + { + $table = $this->prepareTable(); + $api = $endpoint->api(); + foreach ($api->getPackages() as $package) { + if ($package->name !== $packageName) { + continue; + } + foreach ($package->stages as $stage) { + $label = [$stage]; + if ($stage === $package->{'active-stage'}) { + $label[] = Html::tag('small', [' (', $this->translate('active'), ')']); + } + + $table->add(Table::row([ + Link::create($label, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package->name, + 'stage' => $stage + ]) + ])); + } + } + + return $table; + } + + protected function getFiles(IcingaEndpoint $endpoint, $package, $stage) + { + $table = $this->prepareTable(); + $table->getAttributes()->set('data-base-target', '_next'); + foreach ($endpoint->api()->listStageFiles($stage, $package) as $filename) { + $table->add($table->row([ + Link::create($filename, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package, + 'stage' => $stage, + 'file' => $filename + ]) + ])); + } + + return $table; + } + + protected function getFile(IcingaEndpoint $endpoint, $package, $stage, $file) + { + return Html::tag('pre', $endpoint->api()->getStagedFile($stage, $file, $package)); + } + + protected function prepareTable($headerCols = []) + { + $table = new Table(); + $table->addAttributes([ + 'class' => ['common-table', 'table-row-selectable'], + 'data-base-target' => '_self' + ]); + if (! empty($headerCols)) { + $table->add($table::row($headerCols, null, 'th')); + } + + return $table; + } +} diff --git a/public/css/module.less b/public/css/module.less index e74e0f47..686217be 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -1453,6 +1453,132 @@ input[type=submit].icon-button { } } +/** BEGIN breadcrumb **/ +// Hint: .badges is unused right now +.breadcrumb { + list-style: none; + overflow: hidden; + padding: 0; + + .badges { + display: inline-block; + padding: 0 0 0 0.5em; + .badge { + line-height: 1.25em; + font-size: 0.8em; + border: 1px solid white; + margin: -0.25em 1px 0 0; + } + } +} + +.breadcrumb { + > .critical a { background: @colorCritical; } + > .critical.handled a { background: @colorCriticalHandled; } + > .unknown a { background: @colorUnknown; } + > .unknown.handled a { background: @colorUnknownHandled; } + > .warning a { background: @colorWarning; } + > .warning.handled a { background: @colorWarningHandled; } + > .ok a { background: @colorOk; } +} + +.breadcrumb { + > .critical a:after { border-left-color: @colorCritical; } + > .critical.handled a:after { border-left-color: @colorCriticalHandled; } + > .unknown a:after { border-left-color: @colorUnknown; } + > .unknown.handled a:after { border-left-color: @colorUnknownHandled; } + > .warning a:after { border-left-color: @colorWarning; } + > .warning.handled a:after { border-left-color: @colorWarningHandled; } + > .ok a:after { border-left-color: @colorOk; } +} + +.breadcrumb:after { + content:''; + display:block; + clear: both; +} +.breadcrumb li { + float: left; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + +} +.breadcrumb li a { + // color: white; + color: @icinga-blue; + margin: 0; + // font-size: 1.2em; + text-decoration: none; + padding-left: 2em; + // line-height: 1.5em; + // background: @icinga-blue; + border: 1px solid @icinga-blue; + border-top: none; + border-bottom: none; + position: relative; + display: block; + float: left; + &:focus { + outline: none; + } + &:hover { + text-decoration: none; + } +} +.action-bar .breadcrumb li a { + padding-left: 2em; +} + +.breadcrumb li a:before, .breadcrumb li a:after { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 1.3em solid transparent; + border-bottom: 1.2em solid transparent; + position: absolute; + margin-top: -1.2em; + top: 50%; + left: 100%; +} + +.breadcrumb li a:before { + border-left: 1.2em solid white; + border-left: 1.2em solid @icinga-blue; + margin-left: 1px; + z-index: 1; +} + +.breadcrumb li a:after { + border-left: 1.2em solid @icinga-blue; + border-left: 1.2em solid white; + z-index: 2; +} + +.breadcrumb li:first-child a { + padding-left: 1em; + padding-right: 0.5em; +} +.breadcrumb li:last-child a { + cursor: default; +} +.breadcrumb li:last-child a:hover { + +} + +.breadcrumb li:not(:last-child) a:hover { background: @text-color; color: white; } +.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @text-color; } + +.breadcrumb li a:focus { + text-decoration: underline; +} +/** END of breadcrumb **/ + + + ul.filter-root { margin-top: 0; width: 100%;