From 4c3600ef6fabfe869d394533fcbef2bc7c59a8ed Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Mon, 17 Mar 2025 13:45:28 +0100 Subject: [PATCH 1/6] Timeline: Visualize future rotations - Introduce class FutureEntry --- .../Widget/TimeGrid/BaseGrid.php | 15 +++++ library/Notifications/Widget/Timeline.php | 7 +++ .../Widget/Timeline/FutureEntry.php | 50 +++++++++++++++++ public/css/timeline.less | 56 +++++++++++++++---- 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 library/Notifications/Widget/Timeline/FutureEntry.php diff --git a/library/Notifications/Widget/TimeGrid/BaseGrid.php b/library/Notifications/Widget/TimeGrid/BaseGrid.php index 7fc94f4cc..26f5e984c 100644 --- a/library/Notifications/Widget/TimeGrid/BaseGrid.php +++ b/library/Notifications/Widget/TimeGrid/BaseGrid.php @@ -7,6 +7,7 @@ use DateInterval; use DateTime; use Generator; +use Icinga\Module\Notifications\Widget\Timeline\FutureEntry; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; @@ -421,6 +422,20 @@ final protected function yieldFixedEntries(Traversable $entries): Generator } $rowStart = $position + $rowStartModifier; + + if ($entry instanceof FutureEntry) { + $gridArea = $this->getGridArea( + $rowStart, + $rowStart + 1, + 1, + $gridBorderAt + 1 + ); + + yield $gridArea => $entry; + + continue; + } + if ($rowStart > $lastRow) { $lastRow = $rowStart; } diff --git a/library/Notifications/Widget/Timeline.php b/library/Notifications/Widget/Timeline.php index f3cfb46aa..9185ddc36 100644 --- a/library/Notifications/Widget/Timeline.php +++ b/library/Notifications/Widget/Timeline.php @@ -14,6 +14,7 @@ use Icinga\Module\Notifications\Widget\TimeGrid\Timescale; use Icinga\Module\Notifications\Widget\TimeGrid\Util; use Icinga\Module\Notifications\Widget\Timeline\Entry; +use Icinga\Module\Notifications\Widget\Timeline\FutureEntry; use Icinga\Module\Notifications\Widget\Timeline\MinimalGrid; use Icinga\Module\Notifications\Widget\Timeline\Rotation; use IntlDateFormatter; @@ -173,7 +174,9 @@ public function getEntries(): Traversable $occupiedCells = []; foreach ($rotations as $rotation) { + $entryFound = false; foreach ($rotation->fetchTimeperiodEntries($this->start, $this->getGrid()->getGridEnd()) as $entry) { + $entryFound = true; if (! $this->minimalLayout) { $entry->setPosition($maxPriority - $rotation->getPriority()); @@ -182,6 +185,10 @@ public function getEntries(): Traversable $occupiedCells += $getDesiredCells($entry); } + + if (! $entryFound && ! $this->minimalLayout) { + yield (new FutureEntry())->setPosition($maxPriority - $rotation->getPriority()); + } } $entryToCellsMap = new SplObjectStorage(); diff --git a/library/Notifications/Widget/Timeline/FutureEntry.php b/library/Notifications/Widget/Timeline/FutureEntry.php new file mode 100644 index 000000000..621344f74 --- /dev/null +++ b/library/Notifications/Widget/Timeline/FutureEntry.php @@ -0,0 +1,50 @@ + + */ +class FutureEntry extends Entry +{ + public function __construct() + { + parent::__construct(0); + + $this->setContinuationType(Entry::TO_NEXT_GRID); + } + + public function getColor(int $transparency): string + { + // --base-disabled (#d0d3da) -> hsl(222, 12%, 84%) + transparency + return sprintf('~"hsl(222 12%% 84%% / %d%%)"', $transparency); + } + + protected function assembleContainer(BaseHtmlElement $container): void + { + $futureBadge = new HtmlElement( + 'div', + new Attributes([ + 'title' => $this->translate('Rotation starts in the future'), + $container->getAttributes()->get('class') + ]), + new Icon('angle-right') + ); + + $container + ->setAttribute('class', 'future-entry') // override the default class + ->addHtml($futureBadge); + } +} diff --git a/public/css/timeline.less b/public/css/timeline.less index 8f3de51d6..d8cf1fbd2 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -44,19 +44,53 @@ } } - .overlay .entry { - margin-top: 1em; - margin-bottom: 1em; - z-index: 2; // overlap the .clock .time-hand + .overlay { + .entry { + margin-top: 1em; + margin-bottom: 1em; + z-index: 2; // overlap the .clock .time-hand + + .title { + height: 100%; + flex-wrap: nowrap; + align-items: baseline; + padding: .15em .5em; + + .name { + .text-ellipsis(); + } + } + } - .title { - height: 100%; - flex-wrap: nowrap; - align-items: baseline; - padding: .15em .5em; + .future-entry { + display: flex; + justify-content: end; - .name { - .text-ellipsis(); + .entry { + display: flex; + align-items: center; + justify-content: end; + flex-shrink: 0; + position: relative; + padding-right: .25em; + width: 3em; + + &:before, + &:after { + content: ''; + display: block; + position: absolute; + border: 1px solid var(--entry-border-color); + top: -1px; + bottom: -1px; + left: 2px; + width: 100%; + .rounded-corners(0.25em); + } + + &:after { + left: 5px; // 2px before + 1px border + 2px after + } } } } From 01cf1221d2cf4a928f78c8f3e26681d9681467fb Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Oct 2025 17:28:24 +0200 Subject: [PATCH 2/6] BaseGrid: Remove timeline specifics Timeline is a separate implementation. Base grid is what it's name implies, a base implementation. --- .../Notifications/Widget/TimeGrid/BaseGrid.php | 15 --------------- library/Notifications/Widget/Timeline.php | 5 ++++- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/library/Notifications/Widget/TimeGrid/BaseGrid.php b/library/Notifications/Widget/TimeGrid/BaseGrid.php index 26f5e984c..7fc94f4cc 100644 --- a/library/Notifications/Widget/TimeGrid/BaseGrid.php +++ b/library/Notifications/Widget/TimeGrid/BaseGrid.php @@ -7,7 +7,6 @@ use DateInterval; use DateTime; use Generator; -use Icinga\Module\Notifications\Widget\Timeline\FutureEntry; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; @@ -422,20 +421,6 @@ final protected function yieldFixedEntries(Traversable $entries): Generator } $rowStart = $position + $rowStartModifier; - - if ($entry instanceof FutureEntry) { - $gridArea = $this->getGridArea( - $rowStart, - $rowStart + 1, - 1, - $gridBorderAt + 1 - ); - - yield $gridArea => $entry; - - continue; - } - if ($rowStart > $lastRow) { $lastRow = $rowStart; } diff --git a/library/Notifications/Widget/Timeline.php b/library/Notifications/Widget/Timeline.php index 9185ddc36..f185d3092 100644 --- a/library/Notifications/Widget/Timeline.php +++ b/library/Notifications/Widget/Timeline.php @@ -187,7 +187,10 @@ public function getEntries(): Traversable } if (! $entryFound && ! $this->minimalLayout) { - yield (new FutureEntry())->setPosition($maxPriority - $rotation->getPriority()); + yield (new FutureEntry()) + ->setStart($this->getGrid()->getGridStart()) + ->setEnd($this->getGrid()->getGridEnd()) + ->setPosition($maxPriority - $rotation->getPriority()); } } From 123e6ab3582dfd7b9ef9ae446ae940095abae90a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Oct 2025 17:29:37 +0200 Subject: [PATCH 3/6] DynamicGrid: Ensure grid and overlay matches always In case there are less rows than sidebar entries, the grids do not match and hence the minimum must be the amount of sidebar entries that already applies in case the overlay is empty. --- library/Notifications/Widget/TimeGrid/DynamicGrid.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Notifications/Widget/TimeGrid/DynamicGrid.php b/library/Notifications/Widget/TimeGrid/DynamicGrid.php index 7825e1a7e..91839ae31 100644 --- a/library/Notifications/Widget/TimeGrid/DynamicGrid.php +++ b/library/Notifications/Widget/TimeGrid/DynamicGrid.php @@ -129,7 +129,7 @@ protected function assemble() ]); $overlay = $this->createGridOverlay(); - if ($overlay->isEmpty()) { + if ($overlay->isEmpty() || count($overlay) < count($this->sideBar())) { $this->style->addFor($this, [ '--primaryRows' => count($this->sideBar()) ]); From ed9a431e08d4b7a0e8944f4b74ca7e6911ff1d2a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 2 Oct 2025 17:31:06 +0200 Subject: [PATCH 4/6] FutureEntry: Simplify implementation If it's not an `.entry`, that's fine. But making the child one, is not. Only children of the overlay must be entries. If style rules apply, copy them over. --- .../Widget/Timeline/FutureEntry.php | 19 +++++++------------ public/css/timeline.less | 6 +++++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/library/Notifications/Widget/Timeline/FutureEntry.php b/library/Notifications/Widget/Timeline/FutureEntry.php index 621344f74..68cc0d673 100644 --- a/library/Notifications/Widget/Timeline/FutureEntry.php +++ b/library/Notifications/Widget/Timeline/FutureEntry.php @@ -14,16 +14,16 @@ * FutureEntry * * Visualize a future entry of the rotation - * - * @extends Entry<0> */ class FutureEntry extends Entry { + protected $defaultAttributes = ['class' => 'future-entry']; + + protected $continuationType = Entry::TO_NEXT_GRID; + public function __construct() { parent::__construct(0); - - $this->setContinuationType(Entry::TO_NEXT_GRID); } public function getColor(int $transparency): string @@ -34,17 +34,12 @@ public function getColor(int $transparency): string protected function assembleContainer(BaseHtmlElement $container): void { - $futureBadge = new HtmlElement( + $container->addHtml(new HtmlElement( 'div', new Attributes([ - 'title' => $this->translate('Rotation starts in the future'), - $container->getAttributes()->get('class') + 'title' => $this->translate('Rotation starts in the future') ]), new Icon('angle-right') - ); - - $container - ->setAttribute('class', 'future-entry') // override the default class - ->addHtml($futureBadge); + )); } } diff --git a/public/css/timeline.less b/public/css/timeline.less index d8cf1fbd2..099168d21 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -65,8 +65,12 @@ .future-entry { display: flex; justify-content: end; + z-index: 2; // overlap the .clock .time-hand + pointer-events: all; - .entry { + > div { + margin-top: 1em; + margin-bottom: 1em; display: flex; align-items: center; justify-content: end; From 8a496ee9200b7cc71253209bc2ef6637e049cb92 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 6 Oct 2025 10:01:45 +0200 Subject: [PATCH 5/6] FutureEntry: Let CSS handle its border color --- library/Notifications/Widget/Timeline/FutureEntry.php | 3 +-- public/css/timeline.less | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/library/Notifications/Widget/Timeline/FutureEntry.php b/library/Notifications/Widget/Timeline/FutureEntry.php index 68cc0d673..8df73756d 100644 --- a/library/Notifications/Widget/Timeline/FutureEntry.php +++ b/library/Notifications/Widget/Timeline/FutureEntry.php @@ -28,8 +28,7 @@ public function __construct() public function getColor(int $transparency): string { - // --base-disabled (#d0d3da) -> hsl(222, 12%, 84%) + transparency - return sprintf('~"hsl(222 12%% 84%% / %d%%)"', $transparency); + return ''; // No user, no color, CSS will handle it } protected function assembleContainer(BaseHtmlElement $container): void diff --git a/public/css/timeline.less b/public/css/timeline.less index 099168d21..45a1daa63 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -84,7 +84,7 @@ content: ''; display: block; position: absolute; - border: 1px solid var(--entry-border-color); + border: 1px solid @gray-light; top: -1px; bottom: -1px; left: 2px; From 07db3162533ad510704ab3c62200d3e5e019a037 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 6 Oct 2025 10:02:47 +0200 Subject: [PATCH 6/6] FutureEntry: Actually let it look like on the mockups --- .../Widget/Timeline/FutureEntry.php | 5 +++- public/css/timeline.less | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/library/Notifications/Widget/Timeline/FutureEntry.php b/library/Notifications/Widget/Timeline/FutureEntry.php index 8df73756d..74f26c31f 100644 --- a/library/Notifications/Widget/Timeline/FutureEntry.php +++ b/library/Notifications/Widget/Timeline/FutureEntry.php @@ -38,7 +38,10 @@ protected function assembleContainer(BaseHtmlElement $container): void new Attributes([ 'title' => $this->translate('Rotation starts in the future') ]), - new Icon('angle-right') + new Icon('angle-right'), + new HtmlElement('span', new Attributes(['class' => 'outline'])), + new HtmlElement('span', new Attributes(['class' => 'outline'])), + new HtmlElement('span', new Attributes(['class' => 'outline'])) )); } } diff --git a/public/css/timeline.less b/public/css/timeline.less index 45a1daa63..ff156a820 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -74,26 +74,35 @@ display: flex; align-items: center; justify-content: end; - flex-shrink: 0; position: relative; - padding-right: .25em; width: 3em; - &:before, - &:after { + .outline { content: ''; display: block; position: absolute; border: 1px solid @gray-light; + border-right-width: 0; top: -1px; bottom: -1px; - left: 2px; width: 100%; - .rounded-corners(0.25em); + .rounded-corners(0.25em 0 0 0.25em); } - &:after { - left: 5px; // 2px before + 1px border + 2px after + .icon { + color: @gray; + } + + .outline:nth-of-type(1) { + left: -1px; // Dock at the grid border + } + + .outline:nth-of-type(2) { + left: 3px; // 4px gap + } + + .outline:nth-of-type(3) { + left: 7px; // Same gap } } }