['object-detail', 'host-detail'], 'data-pdfexport-page-breaks-at' => 'h2' ]; protected $tag = 'div'; public function __construct($object) { $this->object = $object; $this->objectType = $object instanceof Host ? 'host' : 'service'; } protected function compatObject() { if ($this->compatObject === null) { $this->compatObject = CompatHost::fromModel($this->object); } return $this->compatObject; } protected function createPrintHeader() { $info = [new HorizontalKeyValue(t('Name'), $this->object->name)]; if ($this->objectType === 'host') { $info[] = new HorizontalKeyValue( t('IPv4 Address'), $this->object->address ?: new EmptyState(t('None', 'address')) ); $info[] = new HorizontalKeyValue( t('IPv6 Address'), $this->object->address6 ?: new EmptyState(t('None', 'address')) ); } $info[] = new HorizontalKeyValue(t('State'), [ $this->object->state->getStateTextTranslated(), ' ', new StateBall($this->object->state->getStateText()) ]); $info[] = new HorizontalKeyValue( t('Last State Change'), DateFormatter::formatDateTime($this->object->state->last_state_change->getTimestamp()) ); return [ new HtmlElement('h2', null, Text::create( $this->objectType === 'host' ? t('Host') : t('Service') )), $info ]; } protected function createActions() { $this->fetchCustomVars(); $navigation = new Navigation(); $navigation->load('icingadb-' . $this->objectType . '-action'); /** @var Action $item */ foreach ($navigation as $item) { $item->setObject($this->object); } $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); $obj = $monitoringInstalled ? $this->compatObject() : $this->object; foreach ($this->object->action_url->first()->action_url ?? [] as $url) { $url = $this->expandMacros($url, $obj); $navigation->addItem( Html::wantHtml([ // Add warning to links that open in new tabs, as recommended by WCAG20 G201 new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), $url ])->render(), [ 'target' => '_blank', 'url' => $url, 'renderer' => [ 'NavigationItemRenderer', 'escape_label' => false ] ] ); } $moduleActions = ObjectActionsHook::loadActions($this->object); $nativeExtensionProviders = []; foreach ($moduleActions->getContent() as $item) { if ($item->getAttributes()->has('data-icinga-module')) { $nativeExtensionProviders[$item->getAttributes()->get('data-icinga-module')->getValue()] = true; } } if (Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { foreach (Hook::all('Monitoring\\' . ucfirst($this->objectType) . 'Actions') as $hook) { $moduleName = ClassLoader::extractModuleName(get_class($hook)); if (! isset($nativeExtensionProviders[$moduleName])) { try { $navigation->merge($hook->getNavigation($this->compatObject())); } catch (Exception $e) { Logger::error("Failed to load legacy action hook: %s\n%s", $e, $e->getTraceAsString()); $navigation->addItem($moduleName, ['label' => IcingaException::describe($e), 'url' => '#']); } } } } if ($moduleActions->isEmpty() && ($navigation->isEmpty() || ! $navigation->hasRenderableItems())) { return null; } return [ Html::tag('h2', t('Actions')), new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()), $moduleActions->isEmpty() ? null : $moduleActions ]; } protected function createCheckStatistics(): array { return [ Html::tag('h2', t('Check Statistics')), new CheckStatistics($this->object) ]; } protected function createComments(): array { if ($this->objectType === 'host') { $link = HostLinks::comments($this->object); $relations = ['host', 'host.state']; } else { $link = ServiceLinks::comments($this->object, $this->object->host); $relations = ['service', 'service.state', 'service.host', 'service.host.state']; } $comments = $this->object->comment ->with($relations) ->limit(3) ->peekAhead(); // TODO: This should be automatically done by the model/resolver and added as ON condition $comments->filter(Filter::equal('object_type', $this->objectType)); $comments = $comments->execute(); /** @var ResultSet $comments */ $content = [Html::tag('h2', t('Comments'))]; if ($comments->hasResult()) { $content[] = (new CommentList($comments))->setObjectLinkDisabled()->setTicketLinkEnabled(); $content[] = (new ShowMore($comments, $link))->setBaseTarget('_next'); } else { $content[] = new EmptyState(t('No comments created.')); } return $content; } protected function createCustomVars(): array { $content = [Html::tag('h2', t('Custom Variables'))]; $this->fetchCustomVars(); $vars = (new CustomvarFlat())->unFlattenVars($this->object->customvar_flat); if (! empty($vars)) { $content[] = new HtmlElement('div', Attributes::create([ 'id' => $this->objectType . '-customvars', 'class' => 'collapsible', 'data-visible-height' => 200 ]), new CustomVarTable($vars, $this->object)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } return $content; } protected function createDowntimes(): array { if ($this->objectType === 'host') { $link = HostLinks::downtimes($this->object); $relations = ['host', 'host.state']; } else { $link = ServiceLinks::downtimes($this->object, $this->object->host); $relations = ['service', 'service.state', 'service.host', 'service.host.state']; } $downtimes = $this->object->downtime ->with($relations) ->limit(3) ->peekAhead(); // TODO: This should be automatically done by the model/resolver and added as ON condition $downtimes->filter(Filter::equal('object_type', $this->objectType)); $downtimes = $downtimes->execute(); /** @var ResultSet $downtimes */ $content = [Html::tag('h2', t('Downtimes'))]; if ($downtimes->hasResult()) { $content[] = (new DowntimeList($downtimes))->setObjectLinkDisabled()->setTicketLinkEnabled(); $content[] = (new ShowMore($downtimes, $link))->setBaseTarget('_next'); } else { $content[] = new EmptyState(t('No downtimes scheduled.')); } return $content; } protected function createGroups(): array { $groups = [Html::tag('h2', t('Groups'))]; if ($this->objectType === 'host') { $hostgroups = []; if ($this->isPermittedRoute('hostgroups')) { $hostgroups = $this->object->hostgroup; $this->applyRestrictions($hostgroups); } $hostgroupList = new TagList(); foreach ($hostgroups as $hostgroup) { $hostgroupList->addLink($hostgroup->display_name, Links::hostgroup($hostgroup)); } $groups[] = $hostgroupList->hasContent() ? $hostgroupList : new EmptyState(t('Not a member of any host group.')); } else { $servicegroups = []; if ($this->isPermittedRoute('servicegroups')) { $servicegroups = $this->object->servicegroup; $this->applyRestrictions($servicegroups); } $servicegroupList = new TagList(); foreach ($servicegroups as $servicegroup) { $servicegroupList->addLink($servicegroup->display_name, Links::servicegroup($servicegroup)); } $groups[] = $servicegroupList->hasContent() ? $servicegroupList : new EmptyState(t('Not a member of any service group.')); } return $groups; } protected function createNotes() { $navigation = new Navigation(); $notes = trim($this->object->notes); $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); $obj = $monitoringInstalled ? $this->compatObject() : $this->object; foreach ($this->object->notes_url->first()->notes_url ?? [] as $url) { $url = $this->expandMacros($url, $obj); $navigation->addItem( Html::wantHtml([ // Add warning to links that open in new tabs, as recommended by WCAG20 G201 new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), $url ])->render(), [ 'target' => '_blank', 'url' => $url, 'renderer' => [ 'NavigationItemRenderer', 'escape_label' => false ] ] ); } $content = []; if (! $navigation->isEmpty() && $navigation->hasRenderableItems()) { $content[] = new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()); } if ($notes !== '') { $content[] = (new MarkdownText($notes)) ->addAttributes([ 'class' => 'collapsible', 'data-visible-height' => 200, 'id' => $this->objectType . '-notes' ]); } if (empty($content)) { return null; } array_unshift($content, Html::tag('h2', t('Notes'))); return $content; } protected function createNotifications(): array { list($users, $usergroups) = $this->getUsersAndUsergroups(); $userList = new TagList(); $usergroupList = new TagList(); foreach ($users as $user) { $userList->addLink([new Icon(Icons::USER), $user->display_name], Links::user($user)); } foreach ($usergroups as $usergroup) { $usergroupList->addLink( [new Icon(Icons::USERGROUP), $usergroup->display_name], Links::usergroup($usergroup) ); } return [ Html::tag('h2', t('Notifications')), new HorizontalKeyValue( t('Users'), $userList->hasContent() ? $userList : new EmptyState(t('No users configured.')) ), new HorizontalKeyValue( t('User Groups'), $usergroupList->hasContent() ? $usergroupList : new EmptyState(t('No user groups configured.')) ) ]; } protected function createPerformanceData(): array { $content[] = Html::tag('h2', t('Performance Data')); if (empty($this->object->state->performance_data)) { $content[] = new EmptyState(t('No performance data available.')); } else { $content[] = new HtmlElement( 'div', Attributes::create(['id' => 'check-perfdata-' . $this->object->checkcommand_name]), new PerfDataTable($this->object->state->normalized_performance_data) ); } return $content; } protected function createPluginOutput(): array { if (empty($this->object->state->output) && empty($this->object->state->long_output)) { $pluginOutput = new EmptyState(t('Output unavailable.')); } else { $pluginOutput = new PluginOutputContainer( PluginOutput::fromObject($this->object) ->setCharacterLimit(10000) ); CopyToClipboard::attachTo($pluginOutput); } return [ Html::tag('h2', t('Plugin Output')), Html::tag( 'div', [ 'id' => 'check-output-' . $this->object->checkcommand_name, 'class' => 'collapsible', 'data-visible-height' => 100 ], $pluginOutput ) ]; } protected function createExtensions(): array { $extensions = ObjectDetailExtensionHook::loadExtensions($this->object); $nativeExtensionProviders = []; foreach ($extensions as $extension) { if ($extension instanceof BaseHtmlElement && $extension->getAttributes()->has('data-icinga-module')) { $nativeExtensionProviders[$extension->getAttributes()->get('data-icinga-module')->getValue()] = true; } } if (! Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { return $extensions; } foreach (Hook::all('Grapher') as $grapher) { /** @var GrapherHook $grapher */ $moduleName = ClassLoader::extractModuleName(get_class($grapher)); if (isset($nativeExtensionProviders[$moduleName])) { continue; } try { $graph = HtmlString::create($grapher->getPreviewHtml($this->compatObject())); } catch (Exception $e) { Logger::error("Failed to load legacy grapher: %s\n%s", $e, $e->getTraceAsString()); $graph = Text::create(IcingaException::describe($e)); } $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::GRAPH_SECTION]; while (isset($extensions[$location])) { $location++; } $extensions[$location] = $graph; } foreach (Hook::all('Monitoring\DetailviewExtension') as $extension) { /** @var DetailviewExtensionHook $extension */ $moduleName = $extension->getModule()->getName(); if (isset($nativeExtensionProviders[$moduleName])) { continue; } try { /** @var Web $app */ $app = Icinga::app(); $renderedExtension = $extension ->setView($app->getViewRenderer()->view) ->getHtmlForObject($this->compatObject()); $extensionHtml = new HtmlElement( 'div', Attributes::create([ 'class' => 'icinga-module module-' . $moduleName, 'data-icinga-module' => $moduleName ]), HtmlString::create($renderedExtension) ); } catch (Exception $e) { Logger::error("Failed to load legacy detail extension: %s\n%s", $e, $e->getTraceAsString()); $extensionHtml = Text::create(IcingaException::describe($e)); } $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::DETAIL_SECTION]; while (isset($extensions[$location])) { $location++; } $extensions[$location] = $extensionHtml; } return $extensions; } protected function createFeatureToggles(): array { $form = new ToggleObjectFeaturesForm($this->object); if ($this->objectType === 'host') { $form->setAction(HostLinks::toggleFeatures($this->object)->getAbsoluteUrl()); } else { $form->setAction(ServiceLinks::toggleFeatures($this->object, $this->object->host)->getAbsoluteUrl()); } return [ Html::tag('h2', t('Feature Commands')), $form ]; } protected function getUsersAndUsergroups(): array { $users = []; $usergroups = []; $groupBy = false; if ($this->objectType === 'host') { $objectFilter = Filter::all( Filter::equal('notification.host_id', $this->object->id), Filter::unlike('notification.service_id', '*') ); $objectFilter->metaData()->set('forceOptimization', false); $groupBy = true; } else { $objectFilter = Filter::equal( 'notification.service_id', $this->object->id ); } $userQuery = null; if ($this->isPermittedRoute('users')) { $userQuery = User::on($this->getDb()); $userQuery->filter($objectFilter); $this->applyRestrictions($userQuery); if ($groupBy) { $userQuery->getSelectBase()->groupBy(['user.id']); } foreach ($userQuery as $user) { $users[$user->name] = $user; } } if ($this->isPermittedRoute('usergroups')) { $usergroupQuery = Usergroup::on($this->getDb()); $usergroupQuery->filter($objectFilter); $this->applyRestrictions($usergroupQuery); if ($groupBy && $userQuery !== null) { $userQuery->getSelectBase()->groupBy(['usergroup.id']); } foreach ($usergroupQuery as $usergroup) { $usergroups[$usergroup->name] = $usergroup; } } return [$users, $usergroups]; } protected function fetchCustomVars() { $customvarFlat = $this->object->customvar_flat; if (! $customvarFlat instanceof ResultSet) { $this->applyRestrictions($customvarFlat); $customvarFlat->withColumns(['customvar.name', 'customvar.value']); $this->object->customvar_flat = $customvarFlat->execute(); } } }