Skip to content

Commit 14a94e6

Browse files
authored
Timeline: Visualize future rotations (#309)
fixes #214
2 parents 8b851a8 + 07db316 commit 14a94e6

File tree

4 files changed

+116
-12
lines changed

4 files changed

+116
-12
lines changed

library/Notifications/Widget/TimeGrid/DynamicGrid.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ protected function assemble()
129129
]);
130130

131131
$overlay = $this->createGridOverlay();
132-
if ($overlay->isEmpty()) {
132+
if ($overlay->isEmpty() || count($overlay) < count($this->sideBar())) {
133133
$this->style->addFor($this, [
134134
'--primaryRows' => count($this->sideBar())
135135
]);

library/Notifications/Widget/Timeline.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Icinga\Module\Notifications\Widget\TimeGrid\Timescale;
1515
use Icinga\Module\Notifications\Widget\TimeGrid\Util;
1616
use Icinga\Module\Notifications\Widget\Timeline\Entry;
17+
use Icinga\Module\Notifications\Widget\Timeline\FutureEntry;
1718
use Icinga\Module\Notifications\Widget\Timeline\MinimalGrid;
1819
use Icinga\Module\Notifications\Widget\Timeline\Rotation;
1920
use IntlDateFormatter;
@@ -173,7 +174,9 @@ public function getEntries(): Traversable
173174

174175
$occupiedCells = [];
175176
foreach ($rotations as $rotation) {
177+
$entryFound = false;
176178
foreach ($rotation->fetchTimeperiodEntries($this->start, $this->getGrid()->getGridEnd()) as $entry) {
179+
$entryFound = true;
177180
if (! $this->minimalLayout) {
178181
$entry->setPosition($maxPriority - $rotation->getPriority());
179182

@@ -182,6 +185,13 @@ public function getEntries(): Traversable
182185

183186
$occupiedCells += $getDesiredCells($entry);
184187
}
188+
189+
if (! $entryFound && ! $this->minimalLayout) {
190+
yield (new FutureEntry())
191+
->setStart($this->getGrid()->getGridStart())
192+
->setEnd($this->getGrid()->getGridEnd())
193+
->setPosition($maxPriority - $rotation->getPriority());
194+
}
185195
}
186196

187197
$entryToCellsMap = new SplObjectStorage();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Widget\Timeline;
6+
7+
use Icinga\Module\Notifications\Widget\TimeGrid\Entry;
8+
use ipl\Html\Attributes;
9+
use ipl\Html\BaseHtmlElement;
10+
use ipl\Html\HtmlElement;
11+
use ipl\Web\Widget\Icon;
12+
13+
/**
14+
* FutureEntry
15+
*
16+
* Visualize a future entry of the rotation
17+
*/
18+
class FutureEntry extends Entry
19+
{
20+
protected $defaultAttributes = ['class' => 'future-entry'];
21+
22+
protected $continuationType = Entry::TO_NEXT_GRID;
23+
24+
public function __construct()
25+
{
26+
parent::__construct(0);
27+
}
28+
29+
public function getColor(int $transparency): string
30+
{
31+
return ''; // No user, no color, CSS will handle it
32+
}
33+
34+
protected function assembleContainer(BaseHtmlElement $container): void
35+
{
36+
$container->addHtml(new HtmlElement(
37+
'div',
38+
new Attributes([
39+
'title' => $this->translate('Rotation starts in the future')
40+
]),
41+
new Icon('angle-right'),
42+
new HtmlElement('span', new Attributes(['class' => 'outline'])),
43+
new HtmlElement('span', new Attributes(['class' => 'outline'])),
44+
new HtmlElement('span', new Attributes(['class' => 'outline']))
45+
));
46+
}
47+
}

public/css/timeline.less

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,66 @@
4444
}
4545
}
4646

47-
.overlay .entry {
48-
margin-top: 1em;
49-
margin-bottom: 1em;
50-
z-index: 2; // overlap the .clock .time-hand
47+
.overlay {
48+
.entry {
49+
margin-top: 1em;
50+
margin-bottom: 1em;
51+
z-index: 2; // overlap the .clock .time-hand
52+
53+
.title {
54+
height: 100%;
55+
flex-wrap: nowrap;
56+
align-items: baseline;
57+
padding: .15em .5em;
58+
59+
.name {
60+
.text-ellipsis();
61+
}
62+
}
63+
}
5164

52-
.title {
53-
height: 100%;
54-
flex-wrap: nowrap;
55-
align-items: baseline;
56-
padding: .15em .5em;
65+
.future-entry {
66+
display: flex;
67+
justify-content: end;
68+
z-index: 2; // overlap the .clock .time-hand
69+
pointer-events: all;
5770

58-
.name {
59-
.text-ellipsis();
71+
> div {
72+
margin-top: 1em;
73+
margin-bottom: 1em;
74+
display: flex;
75+
align-items: center;
76+
justify-content: end;
77+
position: relative;
78+
width: 3em;
79+
80+
.outline {
81+
content: '';
82+
display: block;
83+
position: absolute;
84+
border: 1px solid @gray-light;
85+
border-right-width: 0;
86+
top: -1px;
87+
bottom: -1px;
88+
width: 100%;
89+
.rounded-corners(0.25em 0 0 0.25em);
90+
}
91+
92+
.icon {
93+
color: @gray;
94+
}
95+
96+
.outline:nth-of-type(1) {
97+
left: -1px; // Dock at the grid border
98+
}
99+
100+
.outline:nth-of-type(2) {
101+
left: 3px; // 4px gap
102+
}
103+
104+
.outline:nth-of-type(3) {
105+
left: 7px; // Same gap
106+
}
60107
}
61108
}
62109
}

0 commit comments

Comments
 (0)