Introduce Host/Service Renderer

- Use these renderers in ObjectHeader and ObjectList class
- Remove now superfluous Host/ServiceList and ListItem classes
- DetailsAction: Change visibility of url setter to public (These are called in controller now)
This commit is contained in:
Sukhwinder Dhillon 2025-03-19 15:59:29 +01:00 committed by Johannes Meyer
parent 1ff6c25ec6
commit 36ce426bbe
27 changed files with 460 additions and 922 deletions

View file

@ -26,11 +26,10 @@ use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\HostDetail;
use Icinga\Module\Icingadb\Widget\Detail\HostInspectionDetail;
use Icinga\Module\Icingadb\Widget\Detail\HostMetaInfo;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use ipl\Orm\Query;
use ipl\Sql\Expression;
use ipl\Sql\Filter\Exists;
@ -69,10 +68,7 @@ class HostController extends Controller
$this->host = $host;
$this->loadTabsForObject($host);
$this->addControl((new HostList([$host]))
->setViewMode('objectHeader')
->setDetailActionsDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($host));
$this->setTitleTab($this->getRequest()->getActionName());
$this->setTitle($host->display_name);
@ -223,7 +219,7 @@ class HostController extends Controller
yield $this->export($services);
$serviceList = (new ServiceList($services))
$serviceList = (new ObjectList($services))
->setViewMode($viewModeSwitcher->getViewMode());
$this->addControl($paginationControl);
@ -537,7 +533,7 @@ class HostController extends Controller
protected function getDefaultTabControls(): array
{
return [(new HostList([$this->host]))->setDetailActionsDisabled()->setNoSubjectLink()];
return [new ObjectHeader($this->host)];
}
/**

View file

@ -12,7 +12,7 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\HostgroupTableRow;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
@ -109,8 +109,10 @@ class HostgroupController extends Controller
yield $this->export($hosts);
$hostList = (new HostList($hosts->execute()))
->setViewMode($viewModeSwitcher->getViewMode());
$hostList = (new ObjectList($hosts))
->setViewMode($viewModeSwitcher->getViewMode())
->setMultiselectUrl(Links::hostsDetails())
->setDetailUrl(Url::fromPath('icingadb/host'));
// ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
if ($this->getRequest()->getParam('format') === 'pdf') {

View file

@ -15,8 +15,8 @@ use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
use Icinga\Module\Icingadb\Widget\ItemList\HostList;
use Icinga\Module\Icingadb\Widget\HostStatusBar;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\HostItemTable;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ShowMore;
@ -104,8 +104,10 @@ class HostsController extends Controller
$hostList = (new HostItemTable($results, HostItemTable::applyColumnMetaData($hosts, $columns)))
->setSort($sortControl->getSort());
} else {
$hostList = (new HostList($results))
->setViewMode($viewModeSwitcher->getViewMode());
$hostList = (new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode())
->setMultiselectUrl(Links::hostsDetails())
->setDetailUrl(Url::fromPath('icingadb/host'));
}
$this->addContent($hostList);
@ -166,9 +168,8 @@ class HostsController extends Controller
$summary->comments_total = $comments->count();
$this->addControl(
(new HostList($results))
(new ObjectList($results))
->setViewMode('minimal')
->setDetailActionsDisabled()
);
$this->addControl(new ShowMore(
$results,

View file

@ -20,13 +20,13 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use Icinga\Module\Icingadb\Widget\Detail\QuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ServiceDetail;
use Icinga\Module\Icingadb\Widget\Detail\ServiceInspectionDetail;
use Icinga\Module\Icingadb\Widget\Detail\ServiceMetaInfo;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemList\HistoryList;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use ipl\Orm\Query;
use ipl\Sql\Expression;
use ipl\Stdlib\Filter;
@ -79,10 +79,7 @@ class ServiceController extends Controller
$this->service = $service;
$this->loadTabsForObject($service);
$this->addControl((new ServiceList([$service]))
->setViewMode('objectHeader')
->setDetailActionsDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($service));
$this->setTitleTab($this->getRequest()->getActionName());
$this->setTitle(
@ -515,6 +512,6 @@ class ServiceController extends Controller
protected function getDefaultTabControls(): array
{
return [(new ServiceList([$this->service]))->setDetailActionsDisabled()->setNoSubjectLink()];
return [new ObjectHeader($this->service)];
}
}

View file

@ -12,7 +12,7 @@ use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\ServicegroupTableRow;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
@ -117,8 +117,10 @@ class ServicegroupController extends Controller
yield $this->export($services);
$serviceList = (new ServiceList($services->execute()))
->setViewMode($viewModeSwitcher->getViewMode());
$serviceList = (new ObjectList($services))
->setViewMode($viewModeSwitcher->getViewMode())
->setMultiselectUrl(Links::servicesDetails())
->setDetailUrl(Url::fromPath('icingadb/service'));
// ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah...
if ($this->getRequest()->getParam('format') === 'pdf') {

View file

@ -17,7 +17,7 @@ use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions;
use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail;
use Icinga\Module\Icingadb\Widget\ItemList\ServiceList;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ItemTable\ServiceItemTable;
use Icinga\Module\Icingadb\Widget\ServiceStatusBar;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
@ -115,8 +115,10 @@ class ServicesController extends Controller
$serviceList = (new ServiceItemTable($results, ServiceItemTable::applyColumnMetaData($services, $columns)))
->setSort($sortControl->getSort());
} else {
$serviceList = (new ServiceList($results))
->setViewMode($viewModeSwitcher->getViewMode());
$serviceList = (new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode())
->setMultiselectUrl(Links::servicesDetails())
->setDetailUrl(Url::fromPath('icingadb/service'));
}
$this->addContent($serviceList);
@ -182,9 +184,8 @@ class ServicesController extends Controller
$summary->comments_total = $comments->count();
$this->addControl(
(new ServiceList($results))
(new ObjectList($results))
->setViewMode('minimal')
->setDetailActionsDisabled()
);
$this->addControl(new ShowMore(
$results,

View file

@ -0,0 +1,283 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
use Icinga\Module\Icingadb\Widget\IconImage;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use Icinga\Module\Icingadb\Widget\PluginOutputContainer;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\I18n\Translation;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
use ipl\Web\Widget\TimeSince;
/** @implements ItemRenderer<Host|Service> */
abstract class BaseHostAndServiceRenderer implements ItemRenderer
{
use Translation;
abstract protected function createSubject($item, string $layout): ValidHtml;
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
if ($layout === 'header') {
if ($item->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($item->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($item->state->hard_state === $item->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
if ($item instanceof Host) {
$state = HostStates::text($item->state->$stateType);
$previousState = HostStates::text($item->state->$previousStateType);
} else {
$state = ServiceStates::text($item->state->$stateType);
$previousState = ServiceStates::text($item->state->$previousStateType);
}
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($item->state->getIcon());
$stateChange->setHandled(
$item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable)
);
$visual->addHtml($stateChange);
return;
}
$ballSize = $layout === 'minimal' ? StateBall::SIZE_BIG : StateBall::SIZE_LARGE;
$stateBall = new StateBall($item->state->getStateText(), $ballSize);
$stateBall->add($item->state->getIcon());
if ($item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable)) {
$stateBall->getAttributes()->add('class', 'handled');
}
$visual->addHtml($stateBall);
if ($layout !== 'minimal' && $item->state->state_type === 'soft') {
$visual->addHtml(
new CheckAttempt((int) $item->state->check_attempt, (int) $item->max_check_attempts)
);
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
if ($item->state->soft_state === null && $item->state->output === null) {
$caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.')));
} else {
if (empty($item->state->output)) {
$pluginOutput = new EmptyState($this->translate('Output unavailable.'));
} else {
$pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($item));
}
$caption->addHtml($pluginOutput);
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
$title->addHtml(Html::sprintf(
$this->translate('%s is %s', '<hostname> is <state-text>'),
$this->createSubject($item, $layout),
Html::tag('span', ['class' => 'state-text'], $item->state->getStateTextTranslated())
));
if (isset($item->state->affects_children) && $item->state->affects_children) {
$total = (int) $item->total_children;
if ($total > 1000) {
$total = '1000+';
$tooltip = $this->translate('Up to 1000+ affected objects');
} else {
$tooltip = sprintf(
$this->translatePlural(
'%d affected object',
'Up to %d affected objects',
$total
),
$total
);
}
$icon = new Icon(Icons::UNREACHABLE);
$title->addHtml(new HtmlElement(
'span',
Attributes::create([
'class' => 'affected-objects',
'title' => $tooltip
]),
$icon,
Text::create($total)
));
}
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
if ($item->state->is_overdue) {
$since = new TimeSince($item->state->next_update->getTimestamp());
$since->prepend($this->translate('Overdue') . ' ');
$since->prependHtml(new Icon(Icons::WARNING));
$info->addHtml($since);
} elseif ($item->state->last_state_change !== null && $item->state->last_state_change->getTimestamp() > 0) {
$info->addHtml(new TimeSince($item->state->last_state_change->getTimestamp()));
}
}
public function assembleFooter($item, HtmlDocument $footer, string $layout): void
{
$pieChartLimit = 5;
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
$isService = $item instanceof Service;
if (
($isService && $item->state->last_comment->service_id === $item->id)
|| $item->state->last_comment->host_id === $item->id
) {
$comment = $item->state->last_comment;
if ($isService) {
$comment->service = $item;
} else {
$comment->host = $item;
}
$comment = (new CommentList([$comment]))
->setNoSubjectLink()
->setObjectLinkDisabled()
->setDetailActionsDisabled();
$statusIcons->addHtml(
new HtmlElement(
'div',
Attributes::create(['class' => 'comment-wrapper']),
new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment),
(new Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($item->state->is_flapping) {
$title = $isService
? sprintf(
$this->translate('Service "%s" on "%s" is in flapping state'),
$item->display_name, $item->host->display_name
)
: sprintf(
$this->translate('Host "%s" is in flapping state'),
$item->display_name
);
$statusIcons->addHtml(new Icon('random', ['title' => $title]));
}
if (! $item->notifications_enabled) {
$statusIcons->addHtml(
new Icon('bell-slash', ['title' => $this->translate('Notifications disabled')])
);
}
if (! $item->active_checks_enabled) {
$statusIcons->addHtml(
new Icon('eye-slash', ['title' => $this->translate('Active checks disabled')])
);
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than $PIE_CHART_LIMIT
if (count($pies) > $pieChartLimit) {
break;
}
}
$maxVisiblePies = $pieChartLimit - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > $pieChartLimit
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
public function assemble($item, string $name, HtmlDocument $element, string $layout): bool
{
if ($name === 'icon-image') {
if (isset($item->icon_image->icon_image)) {
$element->addHtml(new IconImage($item->icon_image->icon_image, $item->icon_image_alt));
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,29 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Html\Attributes;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Web\Widget\Link;
class HostRenderer extends BaseHostAndServiceRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('host');
}
protected function createSubject($item, string $layout): ValidHtml
{
if ($layout === 'header') {
return new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($item->display_name));
}
return new Link($item->display_name, Links::host($item), ['class' => 'subject']);
}
}

View file

@ -0,0 +1,41 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
class ServiceRenderer extends BaseHostAndServiceRenderer
{
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->get('class')->addValue('service');
}
protected function createSubject($item, string $layout): ValidHtml
{
$service = $item->display_name;
$host = [
new StateBall($item->host->state->getStateText(), StateBall::SIZE_MEDIUM),
' ',
$item->host->display_name
];
$host = new Link($host, Links::host($item->host), ['class' => 'subject']);
if ($layout === 'header') {
$service = new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($service));
} else {
$service = new Link($service, Links::service($item, $item->host), ['class' => 'subject']);
}
return Html::sprintf($this->translate('%s on %s', '<service> on <host>'), $service, $host);
}
}

View file

@ -5,8 +5,12 @@
namespace Icinga\Module\Icingadb\Widget\Detail;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
use ipl\Html\BaseHtmlElement;
use ipl\Orm\Model;
use ipl\Web\Layout\HeaderItemLayout;
@ -28,6 +32,15 @@ class ObjectHeader extends BaseHtmlElement
switch (true) {
case $this->object instanceof RedundancyGroup:
$renderer = new RedundancyGroupRenderer();
break;
case $this->object instanceof Service:
$renderer = new ServiceRenderer();
break;
case $this->object instanceof Host:
$renderer = new HostRenderer();
break;
default:
throw new NotImplementedError('Not implemented');

View file

@ -1,56 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Model\Host;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Link;
/**
* Host item of a host list. Represents one database row.
*
* @property Host $item
* @property HostList $list
*/
abstract class BaseHostListItem extends StateListItem
{
use NoSubjectLink;
/**
* Create new subject link
*
* @return BaseHtmlElement
*/
protected function createSubject()
{
if ($this->getNoSubjectLink()) {
return new HtmlElement(
'span',
Attributes::create(['class' => 'subject']),
Text::create($this->item->display_name)
);
} else {
return new Link($this->item->display_name, Links::host($this->item), ['class' => 'subject']);
}
}
protected function init(): void
{
parent::init();
if ($this->list->getNoSubjectLink()) {
$this->setNoSubjectLink();
}
$this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name))
->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name));
}
}

View file

@ -1,70 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
/**
* Service item of a service list. Represents one database row.
*
* @property Service $item
* @property ServiceList $list
*/
abstract class BaseServiceListItem extends StateListItem
{
use NoSubjectLink;
protected function createSubject()
{
$service = $this->item->display_name;
$host = [
new StateBall($this->item->host->state->getStateText(), StateBall::SIZE_MEDIUM),
' ',
$this->item->host->display_name
];
$host = new Link($host, Links::host($this->item->host), ['class' => 'subject']);
if ($this->getNoSubjectLink()) {
$service = new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($service));
} else {
$service = new Link($service, Links::service($this->item, $this->item->host), ['class' => 'subject']);
}
return [Html::sprintf(t('%s on %s', '<service> on <host>'), $service, $host)];
}
protected function init(): void
{
parent::init();
if ($this->list->getNoSubjectLink()) {
$this->setNoSubjectLink();
}
$this->list->addMultiselectFilterAttribute(
$this,
Filter::all(
Filter::equal('service.name', $this->item->name),
Filter::equal('host.name', $this->item->host->name)
)
);
$this->list->addDetailFilterAttribute(
$this,
Filter::all(
Filter::equal('name', $this->item->name),
Filter::equal('host.name', $this->item->host->name)
)
);
}
}

View file

@ -1,69 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\HostStates;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\BaseHtmlElement;
use ipl\Web\Widget\StateBall;
class HostDetailHeader extends HostListItemMinimal
{
protected function getStateBallSize(): string
{
return '';
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
if ($this->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($this->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($this->state->hard_state === $this->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
$state = HostStates::text($this->state->$stateType);
$previousState = HostStates::text($this->state->$previousStateType);
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($this->state->getIcon());
$stateChange->setHandled(
$this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)
);
$visual->addHtml($stateChange);
}
protected function assemble(): void
{
$attributes = $this->list->getAttributes();
if (! in_array('minimal', $attributes->get('class')->getValue())) {
$attributes->add('class', 'minimal');
}
parent::assemble();
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Web\Url;
/**
* Host list
*/
class HostList extends StateList
{
protected $defaultAttributes = ['class' => 'host-list'];
protected function getItemClass(): string
{
switch ($this->getViewMode()) {
case 'minimal':
return HostListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return HostListItemDetailed::class;
case 'objectHeader':
return HostDetailHeader::class;
default:
return HostListItem::class;
}
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::hostsDetails());
$this->setDetailUrl(Url::fromPath('icingadb/host'));
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
use ipl\Web\Widget\StateBall;
class HostListItem extends BaseHostListItem
{
use ListItemCommonLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,108 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class HostListItemDetailed extends BaseHostListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer): void
{
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
if ($this->item->state->last_comment->host_id === $this->item->id) {
$comment = $this->item->state->last_comment;
$comment->host = $this->item;
$comment = (new CommentList([$comment]))
->setNoSubjectLink()
->setObjectLinkDisabled()
->setDetailActionsDisabled();
$statusIcons->addHtml(
new HtmlElement(
'div',
Attributes::create(['class' => 'comment-wrapper']),
new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment),
(new Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($this->item->state->is_flapping) {
$statusIcons->addHtml(new Icon(
'random',
[
'title' => sprintf(t('Host "%s" is in flapping state'), $this->item->display_name),
]
));
}
if (! $this->item->notifications_enabled) {
$statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > HostListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = HostListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > HostListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemMinimalLayout;
use ipl\Web\Widget\StateBall;
class HostListItemMinimal extends BaseHostListItem
{
use ListItemMinimalLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -9,13 +9,17 @@ use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\UnreachableParent;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
use Icinga\Module\Icingadb\Widget\Notice;
use InvalidArgumentException;
use ipl\Html\HtmlDocument;
use ipl\Orm\Model;
use ipl\Stdlib\Filter;
use ipl\Web\Layout\DetailedItemLayout;
use ipl\Web\Layout\ItemLayout;
use ipl\Web\Layout\MinimalItemLayout;
@ -27,7 +31,7 @@ use ipl\Web\Widget\ListItem;
*
* Create a list of icingadb objects
*
* @extends ItemList<RedundancyGroup>
* @extends ItemList<RedundancyGroup> // TODO: fix type
*/
class ObjectList extends ItemList
{
@ -41,6 +45,10 @@ class ObjectList extends ItemList
parent::__construct($data, function (Model $item) {
if ($item instanceof RedundancyGroup) {
return new RedundancyGroupRenderer();
} elseif ($item instanceof Service) {
return new ServiceRenderer();
} elseif ($item instanceof Host) {
return new HostRenderer();
} else {
throw new NotImplementedError('Not implemented');
}
@ -115,32 +123,64 @@ class ObjectList extends ItemList
return $layout;
}
/**
* @param object<UnreachableParent|DependencyNode|RedundancyGroup|Service|Host> $data
*
* @return ListItem
*/
protected function createListItem(object $data)
{
/** @var UnreachableParent|DependencyNode $data */
if ($data->redundancy_group_id !== null) {
return (new ListItem($data->redundancy_group, $this))
->addAttributes(['data-action-item' => true]);
if ($data instanceof DependencyNode) {
if (isset($data->redundancy_group_id)) {
$object = $data->redundancy_group;
} else {
$object = isset($data->service_id) ? $data->service : $data->host;
}
} else {
$object = $data;
}
// TODO: Adjust the remaining stuff once self::getItemLayout supports host and services
$object = $data->service_id !== null ? $data->service : $data->host;
switch (false) {
case MinimalItemLayout::class:
$class = $object instanceof Host ? HostListItemMinimal::class : ServiceListItemMinimal::class;
break;
case DetailedItemLayout::class:
$this->removeAttribute('class', 'default-layout');
$class = $object instanceof Host ? HostListItemDetailed::class : ServiceListItemDetailed::class;
break;
default:
$class = $object instanceof Host ? HostListItem::class : ServiceListItem::class;
if (isset($object->icon_image->icon_image)) {
$this->setHasIconImages(true);
}
return new $class($object, $this);
$item = parent::createListItem($object);
if ($this->getDetailActionsDisabled()) {
return $item;
}
switch (true) {
case $object instanceof RedundancyGroup:
$this->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->id)));
break;
case $object instanceof Service:
$this->addDetailFilterAttribute(
$item,
Filter::all(
Filter::equal('name', $object->name),
Filter::equal('host.name', $object->host->name)
)
);
$this->addMultiSelectFilterAttribute(
$item,
Filter::all(
Filter::equal('service.name', $object->name),
Filter::equal('host.name', $object->host->name)
)
);
break;
case $object instanceof Host:
$this->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
$this->addMultiSelectFilterAttribute($item, Filter::equal('host.name', $object->name));
break;
}
return $item;
}
protected function assemble(): void

View file

@ -1,69 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Module\Icingadb\Widget\StateChange;
use ipl\Html\BaseHtmlElement;
use ipl\Web\Widget\StateBall;
class ServiceDetailHeader extends ServiceListItemMinimal
{
protected function getStateBallSize(): string
{
return '';
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
if ($this->state->state_type === 'soft') {
$stateType = 'soft_state';
$previousStateType = 'previous_soft_state';
if ($this->state->previous_soft_state === 0) {
$previousStateType = 'hard_state';
}
} else {
$stateType = 'hard_state';
$previousStateType = 'previous_hard_state';
if ($this->state->hard_state === $this->state->previous_hard_state) {
$previousStateType = 'previous_soft_state';
}
}
$state = ServiceStates::text($this->state->$stateType);
$previousState = ServiceStates::text($this->state->$previousStateType);
$stateChange = new StateChange($state, $previousState);
if ($stateType === 'soft_state') {
$stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
}
if ($previousStateType === 'previous_soft_state') {
$stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE);
if ($stateType === 'soft_state') {
$visual->getAttributes()->add('class', 'small-state-change');
}
}
$stateChange->setIcon($this->state->getIcon());
$stateChange->setHandled(
$this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)
);
$visual->addHtml($stateChange);
}
protected function assemble(): void
{
$attributes = $this->list->getAttributes();
if (! in_array('minimal', $attributes->get('class')->getValue())) {
$attributes->add('class', 'minimal');
}
parent::assemble();
}
}

View file

@ -1,36 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Links;
use ipl\Web\Url;
class ServiceList extends StateList
{
protected $defaultAttributes = ['class' => 'service-list'];
protected function getItemClass(): string
{
switch ($this->getViewMode()) {
case 'minimal':
return ServiceListItemMinimal::class;
case 'detailed':
$this->removeAttribute('class', 'default-layout');
return ServiceListItemDetailed::class;
case 'objectHeader':
return ServiceDetailHeader::class;
default:
return ServiceListItem::class;
}
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::servicesDetails());
$this->setDetailUrl(Url::fromPath('icingadb/service'));
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemCommonLayout;
use ipl\Web\Widget\StateBall;
class ServiceListItem extends BaseServiceListItem
{
use ListItemCommonLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
}

View file

@ -1,112 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemDetailedLayout;
use Icinga\Module\Icingadb\Util\PerfDataSet;
use Icinga\Module\Icingadb\Widget\ItemList\CommentList;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class ServiceListItemDetailed extends BaseServiceListItem
{
use ListItemDetailedLayout;
/** @var int Max pie charts to be shown */
const PIE_CHART_LIMIT = 5;
protected function getStateBallSize(): string
{
return StateBall::SIZE_LARGE;
}
protected function assembleFooter(BaseHtmlElement $footer): void
{
$statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons']));
if ($this->item->state->last_comment->service_id === $this->item->id) {
$comment = $this->item->state->last_comment;
$comment->service = $this->item;
$comment = (new CommentList([$comment]))
->setNoSubjectLink()
->setObjectLinkDisabled()
->setDetailActionsDisabled();
$statusIcons->addHtml(
new HtmlElement(
'div',
Attributes::create(['class' => 'comment-wrapper']),
new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment),
(new Icon('comments', ['class' => 'comment-icon']))
)
);
}
if ($this->item->state->is_flapping) {
$statusIcons->addHtml(new Icon(
'random',
[
'title' => sprintf(
t('Service "%s" on "%s" is in flapping state'),
$this->item->display_name,
$this->item->host->display_name
),
]
));
}
if (! $this->item->notifications_enabled) {
$statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')]));
}
if (! $this->item->active_checks_enabled) {
$statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')]));
}
$performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data']));
if ($this->item->state->performance_data) {
$pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray();
$pies = [];
foreach ($pieChartData as $i => $perfdata) {
if ($perfdata->isVisualizable()) {
$pies[] = $perfdata->asInlinePie()->render();
}
// Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT
if (count($pies) > ServiceListItemDetailed::PIE_CHART_LIMIT) {
break;
}
}
$maxVisiblePies = ServiceListItemDetailed::PIE_CHART_LIMIT - 2;
$numOfPies = count($pies);
foreach ($pies as $i => $pie) {
if (
// Show max. 5 elements: if there are more than 5, show 4 + `…`
$i > $maxVisiblePies && $numOfPies > ServiceListItemDetailed::PIE_CHART_LIMIT
) {
$performanceData->addHtml(new HtmlElement('span', null, Text::create('…')));
break;
}
$performanceData->addHtml(HtmlString::create($pie));
}
}
if (! $statusIcons->isEmpty()) {
$footer->addHtml($statusIcons);
}
if (! $performanceData->isEmpty()) {
$footer->addHtml($performanceData);
}
}
}

View file

@ -1,18 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemMinimalLayout;
use ipl\Web\Widget\StateBall;
class ServiceListItemMinimal extends BaseServiceListItem
{
use ListItemMinimalLayout;
protected function getStateBallSize(): string
{
return StateBall::SIZE_BIG;
}
}

View file

@ -1,60 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ViewMode;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Widget\Notice;
use ipl\Html\HtmlDocument;
use ipl\Web\Common\BaseItemList;
abstract class StateList extends BaseItemList
{
use ViewMode;
use NoSubjectLink;
use DetailActions;
/** @var bool Whether the list contains at least one item with an icon_image */
protected $hasIconImages = false;
/**
* Get whether the list contains at least one item with an icon_image
*
* @return bool
*/
public function hasIconImages(): bool
{
return $this->hasIconImages;
}
/**
* Set whether the list contains at least one item with an icon_image
*
* @param bool $hasIconImages
*
* @return $this
*/
public function setHasIconImages(bool $hasIconImages): self
{
$this->hasIconImages = $hasIconImages;
return $this;
}
protected function assemble(): void
{
$this->addAttributes(['class' => $this->getViewMode()]);
parent::assemble();
if ($this->data instanceof VolatileStateResults && $this->data->isRedisUnavailable()) {
$this->prependWrapper((new HtmlDocument())->addHtml(new Notice(
t('Redis is currently unavailable. The shown information might be outdated.')
)));
}
}
}

View file

@ -1,174 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Model\State;
use Icinga\Module\Icingadb\Util\PluginOutput;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
use Icinga\Module\Icingadb\Widget\IconImage;
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;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\Text;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
/**
* Host or service item of a host or service list. Represents one database row.
*/
abstract class StateListItem extends BaseListItem
{
use Translation;
/** @var StateList The list where the item is part of */
protected $list;
/** @var State The state of the item */
protected $state;
protected function init(): void
{
$this->state = $this->item->state;
if (isset($this->item->icon_image->icon_image)) {
$this->list->setHasIconImages(true);
}
}
abstract protected function createSubject();
abstract protected function getStateBallSize(): string;
/**
* @return ?BaseHtmlElement
*/
protected function createIconImage(): ?BaseHtmlElement
{
if (! $this->list->hasIconImages()) {
return null;
}
$iconImage = HtmlElement::create('div', [
'class' => 'icon-image',
]);
$this->assembleIconImage($iconImage);
return $iconImage;
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
if ($this->state->soft_state === null && $this->state->output === null) {
$caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.')));
} else {
if (empty($this->state->output)) {
$pluginOutput = new EmptyState($this->translate('Output unavailable.'));
} else {
$pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item));
}
$caption->addHtml($pluginOutput);
}
}
protected function assembleIconImage(BaseHtmlElement $iconImage): void
{
if (isset($this->item->icon_image->icon_image)) {
$iconImage->addHtml(new IconImage($this->item->icon_image->icon_image, $this->item->icon_image_alt));
} else {
$iconImage->addAttributes(['class' => 'placeholder']);
}
}
protected function assembleTitle(BaseHtmlElement $title): void
{
$title->addHtml(Html::sprintf(
$this->translate('%s is %s', '<hostname> is <state-text>'),
$this->createSubject(),
Html::tag('span', ['class' => 'state-text'], $this->state->getStateTextTranslated())
));
if (isset($this->state->affects_children) && $this->state->affects_children) {
$total = (int) $this->item->total_children;
if ($total > 1000) {
$total = '1000+';
$tooltip = $this->translate('Up to 1000+ affected objects');
} else {
$tooltip = sprintf(
$this->translatePlural(
'%d affected object',
'Up to %d affected objects',
$total
),
$total
);
}
$icon = new Icon(Icons::UNREACHABLE);
$title->addHtml(new HtmlElement(
'span',
Attributes::create([
'class' => 'affected-objects',
'title' => $tooltip
]),
$icon,
Text::create($total)
));
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$stateBall = new StateBall($this->state->getStateText(), $this->getStateBallSize());
$stateBall->add($this->state->getIcon());
if ($this->state->is_problem && ($this->state->is_handled || ! $this->state->is_reachable)) {
$stateBall->getAttributes()->add('class', 'handled');
}
$visual->addHtml($stateBall);
if ($this->state->state_type === 'soft') {
$visual->addHtml(
new CheckAttempt((int) $this->state->check_attempt, (int) $this->item->max_check_attempts)
);
}
}
protected function createTimestamp(): ?BaseHtmlElement
{
$since = null;
if ($this->state->is_overdue) {
$since = new TimeSince($this->state->next_update->getTimestamp());
$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());
}
return $since;
}
protected function assemble(): void
{
if ($this->state->is_overdue) {
$this->addAttributes(['class' => 'overdue']);
}
$this->add([
$this->createVisual(),
$this->createIconImage(),
$this->createMain()
]);
}
}

View file

@ -384,7 +384,8 @@ div.show-more {
}
.history-list,
.objectHeader {
.header-item-layout.host,
.header-item-layout.service {
.visual.small-state-change .state-change {
padding-top: .25em;
}

View file

@ -56,10 +56,6 @@
> .empty-state {
padding: .25em;
}
.check-attempt {
display: none; // TODO: The new renderer shouldn't render it in the first place
}
}
.item-list.minimal {
@ -143,7 +139,8 @@
// TODO: Can be simplified (`.controls .list-item`) once the only list-items in controls are those in the multi select views
.controls .item-list:not(.detailed):not(.minimal) .list-item {
.plugin-output {
.plugin-output { //TODO: why ?? multiselect items has bigger gap now
//seems dead code, remove it when cleaning up
line-height: 1.5
}