Introduce DowntimeRenderer

- Use it for ObjectList and ObjectHeader
- Remove now unused code and css
This commit is contained in:
Sukhwinder Dhillon 2025-03-21 15:58:34 +01:00 committed by Johannes Meyer
parent c55f1dceb8
commit c15f32a43f
13 changed files with 327 additions and 360 deletions

View file

@ -10,7 +10,7 @@ use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\Detail\DowntimeDetail;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader;
use ipl\Stdlib\Filter;
use ipl\Web\Url;
@ -61,11 +61,7 @@ class DowntimeController extends Controller
{
$detail = new DowntimeDetail($this->downtime);
$this->addControl((new DowntimeList([$this->downtime]))
->setViewMode('minimal')
->setDetailActionsDisabled()
->setCaptionDisabled()
->setNoSubjectLink());
$this->addControl(new ObjectHeader($this->downtime));
$this->addContent($detail);

View file

@ -10,8 +10,8 @@ use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
@ -87,7 +87,12 @@ class DowntimesController extends Controller
$results = $downtimes->execute();
$this->addContent((new DowntimeList($results))->setViewMode($viewModeSwitcher->getViewMode()));
$this->addContent(
(new ObjectList($results))
->setViewMode($viewModeSwitcher->getViewMode())
->setMultiselectUrl(Links::downtimesDetails())
->setDetailUrl(Url::fromPath('icingadb/downtime'))
);
if ($compact) {
$this->addContent(
@ -162,7 +167,7 @@ class DowntimesController extends Controller
$rs = $downtimes->execute();
$this->addControl((new DowntimeList($rs))->setViewMode('minimal'));
$this->addControl((new ObjectList($rs))->setViewMode('minimal'));
$this->addControl(new ShowMore(
$rs,

View file

@ -0,0 +1,255 @@
<?php
/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\View;
use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use ipl\Html\Attributes;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Common\ItemRenderer;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
/** @implements ItemRenderer<Downtime> */
class DowntimeRenderer implements ItemRenderer
{
use Translation;
use TicketLinks;
use HostLink;
use ServiceLink;
/** @var int Current Time */
protected $currentTime;
/** @var int Duration */
protected $duration;
/** @var int Downtime end time */
protected $endTime;
/** @var bool Whether the downtime is active */
protected $isActive;
/** @var int Downtime start time */
protected $startTime;
/** @var bool Whether the state has been loaded */
protected $stateLoaded = false;
/**
* @var bool Whether the item is being rendered in the detail view of the associated object (host/service)
*
* When true:
*
* - Creation of the link of the associated object (host/service) is omitted from the title
* - The ticket link will be created
*/
protected $isDetailView = false;
/**
* Set whether the item is being rendered in the detail view of the associated object (host/service)
*
* @param bool $state
*
* @return $this
*/
public function setIsDetailView(bool $state = true): self
{
$this->isDetailView = $state;
return $this;
}
/**
* Load the state of the downtime
*
* @param Downtime $item
*
* @return void
*/
protected function loadState(Downtime $item): void
{
if ($this->stateLoaded) {
return;
}
if (
isset($item->start_time, $item->end_time)
&& $item->is_flexible
&& $item->is_in_effect
) {
$this->startTime = $item->start_time->getTimestamp();
$this->endTime = $item->end_time->getTimestamp();
} else {
$this->startTime = $item->scheduled_start_time->getTimestamp();
$this->endTime = $item->scheduled_end_time->getTimestamp();
}
$this->currentTime = time();
$this->isActive = $item->is_in_effect
|| ($item->is_flexible && $item->scheduled_start_time->getTimestamp() <= $this->currentTime);
$until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime;
$this->duration = explode(' ', DateFormatter::formatDuration(
$until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600))
), 2)[0];
$this->stateLoaded = true;
}
public function assembleAttributes($item, Attributes $attributes, string $layout): void
{
$attributes->add(new Attributes(['class' => ['downtime', $item->is_in_effect ? 'in-effect' : '']]));
}
public function assembleVisual($item, HtmlDocument $visual, string $layout): void
{
$this->loadState($item);
$dateTime = DateFormatter::formatDateTime($this->endTime);
if ($this->isActive) {
$visual->addHtml(Html::sprintf(
$this->translate('%s left', '<timespan>..'),
Html::tag(
'strong',
Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
$this->duration
)
)
));
} else {
$visual->addHtml(Html::sprintf(
$this->translate('in %s', '..<timespan>'),
Html::tag('strong', $this->duration)
));
}
}
public function assembleTitle($item, HtmlDocument $title, string $layout): void
{
if ($this->isDetailView) {
$link = null;
} elseif ($item->object_type === 'host') {
$link = $this->createHostLink($item->host, true);
} else {
$link = $this->createServiceLink($item->service, $item->service->host, true);
}
if ($item->is_flexible) {
if ($link !== null) {
$template = $this->translate('{{#link}}Flexible Downtime{{/link}} for %s');
} else {
$template = $this->translate('Flexible Downtime');
}
} else {
if ($link !== null) {
$template = $this->translate('{{#link}}Fixed Downtime{{/link}} for %s');
} else {
$template = $this->translate('Fixed Downtime');
}
}
if ($layout === 'header') {
if ($link === null) {
$title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => HtmlElement::create('span', [ 'class' => 'subject'])],
$link
));
}
} else {
if ($link === null) {
$title->addHtml(new Link($template, Links::downtime($item)));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => new Link('', Links::downtime($item))],
$link
));
}
}
}
public function assembleCaption($item, HtmlDocument $caption, string $layout): void
{
$markdownLine = new MarkdownLine(
$this->isDetailView ? $this->createTicketLinks($item->comment) : $item->comment
);
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->addHtml(
new HtmlElement(
'span',
null,
new Icon(Icons::USER),
Text::create($item->author)
),
Text::create(': ')
)->addFrom($markdownLine);
}
public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void
{
$this->loadState($item);
$dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime);
$info->addHtml(Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
sprintf(
$this->isActive
? $this->translate('expires in %s', '..<timespan>')
: $this->translate('starts in %s', '..<timespan>'),
$this->duration
)
));
}
public function assembleFooter($item, HtmlDocument $footer, string $layout): void
{
}
public function assemble($item, string $name, HtmlDocument $element, string $layout): bool
{
if ($name === 'progress' && ($layout === 'detailed' || $layout === 'common')) {
$this->loadState($item);
$element
->addAttributes(Attributes::create([
'data-animate-progress' => true,
'data-start-time' => $this->startTime,
'data-end-time' => $this->endTime
]))
->addHtml(new HtmlElement('div', Attributes::create(['class' => 'bar'])));
return true;
}
return false;
}
}

View file

@ -10,12 +10,13 @@ use Icinga\Module\Icingadb\Common\Auth;
use Icinga\Module\Icingadb\Common\Database;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Widget\ItemList\ObjectList;
use Icinga\Module\Icingadb\Widget\MarkdownText;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList;
use Icinga\Module\Icingadb\Widget\ShowMore;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyState;
use ipl\Web\Widget\HorizontalKeyValue;
use ipl\Html\BaseHtmlElement;
@ -180,7 +181,9 @@ class DowntimeDetail extends BaseHtmlElement
if ($children->hasResult()) {
$this->addHtml(
new HtmlElement('h2', null, Text::create(t('Children'))),
new DowntimeList($children),
(new ObjectList($children))
->setMultiselectUrl(Links::downtimesDetails())
->setDetailUrl(Url::fromPath('icingadb/downtime')),
(new ShowMore($children, Links::downtimes()->setQueryString(
QueryString::render(Filter::any(
Filter::equal('downtime.parent.name', $this->downtime->name),

View file

@ -285,7 +285,9 @@ class ObjectDetail extends BaseHtmlElement
$content = [Html::tag('h2', t('Downtimes'))];
if ($downtimes->hasResult()) {
$content[] = (new DowntimeList($downtimes))->setObjectLinkDisabled()->setTicketLinkEnabled();
$content[] = (new TicketLinkObjectList($downtimes))
->setMultiselectUrl(Links::downtimesDetails())
->setDetailUrl(Url::fromPath('icingadb/downtime'));
$content[] = (new ShowMore($downtimes, $link))->setBaseTarget('_next');
} else {
$content[] = new EmptyState(t('No downtimes scheduled.'));

View file

@ -6,12 +6,14 @@ namespace Icinga\Module\Icingadb\Widget\Detail;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
@ -59,6 +61,10 @@ class ObjectHeader extends BaseHtmlElement
case $this->object instanceof Comment:
$renderer = new CommentRenderer();
break;
case $this->object instanceof Downtime:
$renderer = new DowntimeRenderer();
break;
default:
throw new NotImplementedError('Not implemented');

View file

@ -1,216 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Common\HostLink;
use Icinga\Module\Icingadb\Common\Icons;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\ServiceLink;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Widget\MarkdownLine;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseListItem;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
/**
* Downtime item of a downtime list. Represents one database row.
*
* @property Downtime $item
* @property DowntimeList $list
*/
abstract class BaseDowntimeListItem extends BaseListItem
{
use HostLink;
use ServiceLink;
use NoSubjectLink;
use ObjectLinkDisabled;
use TicketLinks;
/** @var int Current Time */
protected $currentTime;
/** @var int Duration */
protected $duration;
/** @var int Downtime end time */
protected $endTime;
/** @var bool Whether the downtime is active */
protected $isActive;
/** @var int Downtime start time */
protected $startTime;
protected function init(): void
{
if (
isset($this->item->start_time, $this->item->end_time)
&& $this->item->is_flexible
&& $this->item->is_in_effect
) {
$this->startTime = $this->item->start_time->getTimestamp();
$this->endTime = $this->item->end_time->getTimestamp();
} else {
$this->startTime = $this->item->scheduled_start_time->getTimestamp();
$this->endTime = $this->item->scheduled_end_time->getTimestamp();
}
$this->currentTime = time();
$this->isActive = $this->item->is_in_effect
|| $this->item->is_flexible && $this->item->scheduled_start_time->getTimestamp() <= $this->currentTime;
$until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime;
$this->duration = explode(' ', DateFormatter::formatDuration(
$until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600))
), 2)[0];
$this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name));
$this->list->addMultiselectFilterAttribute($this, Filter::equal('name', $this->item->name));
$this->setObjectLinkDisabled($this->list->getObjectLinkDisabled());
$this->setNoSubjectLink($this->list->getNoSubjectLink());
$this->setTicketLinkEnabled($this->list->getTicketLinkEnabled());
if ($this->item->is_in_effect) {
$this->getAttributes()->add('class', 'in-effect');
}
}
protected function createProgress(): BaseHtmlElement
{
return new HtmlElement(
'div',
Attributes::create([
'class' => 'progress',
'data-animate-progress' => true,
'data-start-time' => $this->startTime,
'data-end-time' => $this->endTime
]),
new HtmlElement(
'div',
Attributes::create(['class' => 'bar'])
)
);
}
protected function assembleCaption(BaseHtmlElement $caption): void
{
$markdownLine = new MarkdownLine($this->createTicketLinks($this->item->comment));
$caption->getAttributes()->add($markdownLine->getAttributes());
$caption->addHtml(
new HtmlElement(
'span',
null,
new Icon(Icons::USER),
Text::create($this->item->author)
),
Text::create(': ')
)->addFrom($markdownLine);
}
protected function assembleTitle(BaseHtmlElement $title): void
{
if ($this->getObjectLinkDisabled()) {
$link = null;
} elseif ($this->item->object_type === 'host') {
$link = $this->createHostLink($this->item->host, true);
} else {
$link = $this->createServiceLink($this->item->service, $this->item->service->host, true);
}
if ($this->item->is_flexible) {
if ($link !== null) {
$template = t('{{#link}}Flexible Downtime{{/link}} for %s');
} else {
$template = t('Flexible Downtime');
}
} else {
if ($link !== null) {
$template = t('{{#link}}Fixed Downtime{{/link}} for %s');
} else {
$template = t('Fixed Downtime');
}
}
if ($this->getNoSubjectLink()) {
if ($link === null) {
$title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => HtmlElement::create('span', [ 'class' => 'subject'])],
$link
));
}
} else {
if ($link === null) {
$title->addHtml(new Link($template, Links::downtime($this->item)));
} else {
$title->addHtml(TemplateString::create(
$template,
['link' => new Link('', Links::downtime($this->item))],
$link
));
}
}
}
protected function assembleVisual(BaseHtmlElement $visual): void
{
$dateTime = DateFormatter::formatDateTime($this->endTime);
if ($this->isActive) {
$visual->addHtml(Html::sprintf(
t('%s left', '<timespan>..'),
Html::tag(
'strong',
Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
$this->duration
)
)
));
} else {
$visual->addHtml(Html::sprintf(
t('in %s', '..<timespan>'),
Html::tag('strong', $this->duration)
));
}
}
protected function createTimestamp(): ?BaseHtmlElement
{
$dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime);
return Html::tag(
'time',
[
'datetime' => $dateTime,
'title' => $dateTime
],
sprintf(
$this->isActive
? t('expires in %s', '..<timespan>')
: t('starts in %s', '..<timespan>'),
$this->duration
)
);
}
}

View file

@ -1,49 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\CaptionDisabled;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Common\Links;
use Icinga\Module\Icingadb\Common\NoSubjectLink;
use Icinga\Module\Icingadb\Common\ObjectLinkDisabled;
use Icinga\Module\Icingadb\Common\TicketLinks;
use Icinga\Module\Icingadb\Common\ViewMode;
use ipl\Web\Common\BaseItemList;
use ipl\Web\Url;
class DowntimeList extends BaseItemList
{
use CaptionDisabled;
use NoSubjectLink;
use ObjectLinkDisabled;
use ViewMode;
use TicketLinks;
use DetailActions;
protected $defaultAttributes = ['class' => 'downtime-list'];
protected function getItemClass(): string
{
$viewMode = $this->getViewMode();
$this->addAttributes(['class' => $viewMode]);
if ($viewMode === 'minimal') {
return DowntimeListItemMinimal::class;
} elseif ($viewMode === 'detailed') {
$this->removeAttribute('class', 'default-layout');
}
return DowntimeListItem::class;
}
protected function init(): void
{
$this->initializeDetailActions();
$this->setMultiselectUrl(Links::downtimesDetails());
$this->setDetailUrl(Url::fromPath('icingadb/downtime'));
}
}

View file

@ -1,23 +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\Html\BaseHtmlElement;
class DowntimeListItem extends BaseDowntimeListItem
{
use ListItemCommonLayout;
protected function assembleMain(BaseHtmlElement $main): void
{
if ($this->item->is_in_effect) {
$main->add($this->createProgress());
}
$main->add($this->createHeader());
$main->add($this->createCaption());
}
}

View file

@ -1,21 +0,0 @@
<?php
/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Module\Icingadb\Common\ListItemMinimalLayout;
class DowntimeListItemMinimal extends BaseDowntimeListItem
{
use ListItemMinimalLayout;
protected function init(): void
{
parent::init();
if ($this->list->isCaptionDisabled()) {
$this->setCaptionDisabled();
}
}
}

View file

@ -8,6 +8,7 @@ use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Common\DetailActions;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\DependencyNode;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\RedundancyGroup;
use Icinga\Module\Icingadb\Model\Service;
@ -16,6 +17,7 @@ use Icinga\Module\Icingadb\Model\User;
use Icinga\Module\Icingadb\Model\Usergroup;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use Icinga\Module\Icingadb\View\HostRenderer;
use Icinga\Module\Icingadb\View\RedundancyGroupRenderer;
use Icinga\Module\Icingadb\View\ServiceRenderer;
@ -61,6 +63,8 @@ class ObjectList extends ItemList
return new UserRenderer();
} elseif ($item instanceof Comment) {
return new CommentRenderer();
} elseif ($item instanceof Downtime) {
return new DowntimeRenderer();
}
throw new NotImplementedError('Not implemented');
@ -132,6 +136,10 @@ class ObjectList extends ItemList
$layout->after(ItemLayout::VISUAL, 'icon-image');
}
if ($item instanceof Downtime) {
$layout->before(ItemLayout::HEADER, 'progress');
}
return $layout;
}
@ -195,7 +203,7 @@ class ObjectList extends ItemList
$this->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
break;
case $object instanceof Comment:
case $object instanceof Comment || $object instanceof Downtime:
$this->addDetailFilterAttribute($item, Filter::equal('name', $object->name));
$this->addMultiSelectFilterAttribute($item, Filter::equal('name', $object->name));

View file

@ -6,7 +6,9 @@ namespace Icinga\Module\Icingadb\Widget\ItemList;
use Icinga\Exception\NotImplementedError;
use Icinga\Module\Icingadb\Model\Comment;
use Icinga\Module\Icingadb\Model\Downtime;
use Icinga\Module\Icingadb\View\CommentRenderer;
use Icinga\Module\Icingadb\View\DowntimeRenderer;
use ipl\Orm\Model;
use ipl\Web\Widget\ItemList;
@ -24,6 +26,8 @@ class TicketLinkObjectList extends ObjectList
ItemList::__construct($data, function (Model $item) {
if ($item instanceof Comment) {
return (new CommentRenderer())->setIsDetailView();
} elseif ($item instanceof Downtime) {
return (new DowntimeRenderer())->setIsDetailView();
}
throw new NotImplementedError('Not implemented');

View file

@ -1,17 +1,22 @@
// Style
.item-layout.downtime {
.visual {
background-color: @gray-lighter;
}
.downtime-list .list-item,
.downtime-detail .list-item {
&.in-effect .visual {
background-color: @color-ok;
color: @text-color-on-icinga-blue;
}
}
.list-item.downtime {
.progress {
> .bar {
background-color: @color-ok;
}
}
.visual {
background-color: @gray-lighter;
}
.main {
border-top: 1px solid @gray-light;
}
@ -23,44 +28,14 @@
border-top: 1px solid @gray-light;
}
}
&.in-effect {
.visual {
background-color: @color-ok;
color: @text-color-on-icinga-blue;
}
.main {
padding-top: 0; // If active the progress bar represents the padding top
}
}
}
// Layout
.downtime-list .list-item {
.item-layout.downtime {
.caption > * {
display: inline;
}
}
.downtime-list .list-item,
.downtime-detail .list-item {
.progress {
height: 2px;
margin-bottom: ~"calc(.5em - 2px)";
min-width: 100%;
position: relative;
> .bar {
height: 100%;
max-width: 100%;
}
}
&:first-child .main .progress > .bar {
height: ~"calc(100% + 1px)"; // +1px due to the border added exclusively for the first item
}
.visual {
justify-content: center;
@ -77,7 +52,29 @@
}
}
.item-list.downtime-list.minimal .list-item {
.list-item.downtime {
.progress {
height: 2px;
margin-bottom: ~"calc(.5em - 2px)";
min-width: 100%;
position: relative;
> .bar {
height: 100%;
max-width: 100%;
}
}
&:first-child .main .progress > .bar {
height: ~"calc(100% + 1px)"; // +1px due to the border added exclusively for the first item
}
.main:has(.progress) {
padding-top: 0;
}
}
.minimal-item-layout.downtime {
.visual {
display: block;
line-height: 1.5;