Skip to content

Commit 78cab5e

Browse files
Timeline: Visualize future rotations
- Introduce class FutureEntry
1 parent 4ce30be commit 78cab5e

File tree

4 files changed

+129
-39
lines changed

4 files changed

+129
-39
lines changed

library/Notifications/Widget/TimeGrid/BaseGrid.php

+40-28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DateInterval;
88
use DateTime;
99
use Generator;
10+
use Icinga\Module\Notifications\Widget\Timeline\FutureEntry;
1011
use ipl\Html\Attributes;
1112
use ipl\Html\BaseHtmlElement;
1213
use ipl\Html\HtmlElement;
@@ -425,46 +426,57 @@ final protected function yieldFixedEntries(Traversable $entries): Generator
425426
$lastRow = $rowStart;
426427
}
427428

428-
$actualStart = Util::roundToNearestThirtyMinute($entry->getStart());
429-
if ($actualStart < $gridStartsAt) {
430-
$colStart = 0;
431-
} else {
432-
$colStart = Util::diffHours($gridStartsAt, $actualStart) * 2;
433-
}
434-
435-
$actualEnd = Util::roundToNearestThirtyMinute($entry->getEnd());
436-
if ($actualEnd > $gridEndsAt) {
429+
if ($entry instanceof FutureEntry) {
430+
$colStart = 1;
437431
$colEnd = $gridBorderAt;
438432
} else {
439-
$colEnd = Util::diffHours($gridStartsAt, $actualEnd) * 2;
440-
}
433+
$actualStart = Util::roundToNearestThirtyMinute($entry->getStart());
434+
if ($actualStart < $gridStartsAt) {
435+
$colStart = 0;
436+
} else {
437+
$colStart = Util::diffHours($gridStartsAt, $actualStart) * 2;
438+
}
439+
440+
$actualEnd = Util::roundToNearestThirtyMinute($entry->getEnd());
441+
if ($actualEnd > $gridEndsAt) {
442+
$colEnd = $gridBorderAt;
443+
} else {
444+
$colEnd = Util::diffHours($gridStartsAt, $actualEnd) * 2;
445+
}
441446

442-
if ($colStart > $gridBorderAt || $colEnd === $colStart) {
443-
throw new LogicException(sprintf(
444-
'Invalid entry (%d) position: %s to %s. Grid dimension: %s to %s',
445-
$entry->getId(),
446-
$actualStart->format('Y-m-d H:i:s'),
447-
$actualEnd->format('Y-m-d H:i:s'),
448-
$gridStartsAt->format('Y-m-d'),
449-
$gridEndsAt->format('Y-m-d')
450-
));
447+
if ($colStart > $gridBorderAt || $colEnd === $colStart) {
448+
throw new LogicException(sprintf(
449+
'Invalid entry (%d) position: %s to %s. Grid dimension: %s to %s',
450+
$entry->getId(),
451+
$actualStart->format('Y-m-d H:i:s'),
452+
$actualEnd->format('Y-m-d H:i:s'),
453+
$gridStartsAt->format('Y-m-d'),
454+
$gridEndsAt->format('Y-m-d')
455+
));
456+
}
457+
458+
$colStart++;
451459
}
452460

453461
$gridArea = $this->getGridArea(
454462
$rowStart,
455463
$rowStart + 1,
456-
$colStart + 1,
464+
$colStart,
457465
$colEnd + 1
458466
);
459467

460-
$fromPrevGrid = $gridStartsAt > $entry->getStart();
461-
$toNextGrid = $gridEndsAt < $entry->getEnd();
462-
if ($fromPrevGrid && $toNextGrid) {
463-
$entry->setContinuationType(Entry::ACROSS_GRID);
464-
} elseif ($fromPrevGrid) {
465-
$entry->setContinuationType(Entry::FROM_PREV_GRID);
466-
} elseif ($toNextGrid) {
468+
if ($entry instanceof FutureEntry) {
467469
$entry->setContinuationType(Entry::TO_NEXT_GRID);
470+
} else {
471+
$fromPrevGrid = $gridStartsAt > $entry->getStart();
472+
$toNextGrid = $gridEndsAt < $entry->getEnd();
473+
if ($fromPrevGrid && $toNextGrid) {
474+
$entry->setContinuationType(Entry::ACROSS_GRID);
475+
} elseif ($fromPrevGrid) {
476+
$entry->setContinuationType(Entry::FROM_PREV_GRID);
477+
} elseif ($toNextGrid) {
478+
$entry->setContinuationType(Entry::TO_NEXT_GRID);
479+
}
468480
}
469481

470482
yield $gridArea => $entry;

library/Notifications/Widget/Timeline.php

+7
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,10 @@ public function getEntries(): Traversable
182185

183186
$occupiedCells += $getDesiredCells($entry);
184187
}
188+
189+
if (! $entryFound && ! $this->minimalLayout) {
190+
yield (new FutureEntry(0))->setPosition($maxPriority - $rotation->getPriority());
191+
}
185192
}
186193

187194
$entryToCellsMap = new SplObjectStorage();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
class FutureEntry extends Entry
14+
{
15+
public function getColor(int $transparency): string
16+
{
17+
return sprintf('~"hsl(166 90%% 50%% / %d%%)"', $transparency);
18+
}
19+
20+
protected function assembleContainer(BaseHtmlElement $container): void
21+
{
22+
$futureBadge = new HtmlElement(
23+
'div',
24+
new Attributes([
25+
'title' => $this->translate('Rotation starts in the future'),
26+
$container->getAttributes()->get('class')
27+
]),
28+
new Icon('angle-right')
29+
);
30+
31+
$container
32+
->setAttribute('class', 'future-entry') // override the default class
33+
->addHtml($futureBadge);
34+
}
35+
}

public/css/timeline.less

+47-11
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,55 @@
4848
}
4949
}
5050

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

56-
.title {
57-
height: 100%;
58-
flex-wrap: nowrap;
59-
align-items: baseline;
60-
padding: .15em .5em;
69+
.future-entry {
70+
display: flex;
71+
justify-content: end;
6172

62-
.name {
63-
.text-ellipsis();
73+
.entry {
74+
display: flex;
75+
align-items: center;
76+
justify-content: center;
77+
78+
position: relative;
79+
padding-left: 6px; // 2px before + 1px border + 2px after + 1px border
80+
81+
width: 2em;
82+
flex-shrink: 0;
83+
84+
&:before,
85+
&:after {
86+
content: '';
87+
display: block;
88+
position: absolute;
89+
border: 1px solid var(--entry-border-color);
90+
border-right: transparent;
91+
height: ~"calc(100% + 2px)"; // border top and bottom
92+
width: 100%;
93+
left: 2px;
94+
.rounded-corners(0.25em);
95+
}
96+
97+
&:after {
98+
left: 5px; // 2px before + 1px border + 2px after
99+
}
64100
}
65101
}
66102
}

0 commit comments

Comments
 (0)