Skip to content

Commit 92d045d

Browse files
Schedule detail: Introduce Timescale
1 parent e72f8d7 commit 92d045d

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Widget\TimeGrid;
6+
7+
use DateTime;
8+
use IntlDateFormatter;
9+
use ipl\Html\Attributes;
10+
use ipl\Html\BaseHtmlElement;
11+
use ipl\Html\HtmlElement;
12+
use ipl\Html\Text;
13+
use ipl\I18n\Translation;
14+
use ipl\Web\Style;
15+
use Locale;
16+
17+
/**
18+
* Creates a localized timescale for the TimeGrid
19+
*/
20+
class Timescale extends BaseHtmlElement
21+
{
22+
use Translation;
23+
24+
protected $tag = 'div';
25+
26+
protected $defaultAttributes = ['class' => 'timescale'];
27+
28+
/** @var int The number of days shown */
29+
protected $days;
30+
31+
/** @var Style */
32+
protected $style;
33+
34+
/**
35+
* Create a new Timescale
36+
*
37+
* @param int $days
38+
* @param Style $style
39+
*/
40+
public function __construct(int $days, Style $style)
41+
{
42+
$this->days = $days;
43+
$this->style = $style;
44+
}
45+
46+
public function assemble(): void
47+
{
48+
switch (true) {
49+
case $this->days === 1:
50+
$timestampPerDay = 24;
51+
break;
52+
case $this->days <= 7:
53+
$timestampPerDay = 3;
54+
break;
55+
case $this->days <= 14:
56+
$timestampPerDay = 2;
57+
break;
58+
default:
59+
$timestampPerDay = 1;
60+
}
61+
62+
$this->style->addFor($this, ['--timestampsPerDay' => $timestampPerDay * 2]); // *2 for .ticks
63+
64+
$dateFormatter = new IntlDateFormatter(
65+
Locale::getDefault(),
66+
IntlDateFormatter::NONE,
67+
IntlDateFormatter::SHORT
68+
);
69+
70+
$timeIntervals = 24 / $timestampPerDay;
71+
72+
$time = new DateTime();
73+
$dayTimestamps = [];
74+
for ($i = 0; $i < $timestampPerDay; $i++) {
75+
// am-pm is separated by non-breaking whitespace
76+
$parts = preg_split('/\s/u', $dateFormatter->format($time->setTime($i * $timeIntervals, 0)));
77+
78+
$stamp = [new HtmlElement('span', null, new Text($parts[0]))];
79+
if (isset($parts[1])) {
80+
$stamp[] = new HtmlElement('span', null, new Text($parts[1]));
81+
}
82+
83+
$dayTimestamps[] = new HtmlElement('span', new Attributes(['class' => 'timestamp']), ...$stamp);
84+
$dayTimestamps[] = new HtmlElement('span', new Attributes(['class' => 'ticks']));
85+
}
86+
87+
$allTimestamps = array_merge(...array_fill(0, $this->days, $dayTimestamps));
88+
// clone is required because $allTimestamps contains references of same object
89+
$allTimestamps[] = (clone $allTimestamps[0])->addAttributes(['class' => 'midnight']); // extra stamp of 12AM
90+
91+
$this->addHtml(...$allTimestamps);
92+
}
93+
}

library/Notifications/Widget/Timeline.php

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Icinga\Module\Notifications\Widget\TimeGrid\DynamicGrid;
1212
use Icinga\Module\Notifications\Widget\TimeGrid\EntryProvider;
1313
use Icinga\Module\Notifications\Widget\TimeGrid\GridStep;
14+
use Icinga\Module\Notifications\Widget\TimeGrid\Timescale;
1415
use Icinga\Module\Notifications\Widget\Timeline\Entry;
1516
use Icinga\Module\Notifications\Widget\Timeline\MinimalGrid;
1617
use Icinga\Module\Notifications\Widget\Timeline\Rotation;
@@ -316,6 +317,9 @@ protected function assemble()
316317
Text::create($this->translate('Result'))
317318
)
318319
);
320+
321+
$this->getGrid()
322+
->addHtml(new Timescale($this->days, $this->getStyle()));
319323
}
320324

321325
$this->addHtml(

public/css/timeline.less

+58-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
.timeline {
44
display: flex;
55
flex-direction: column;
6+
overflow: hidden;
67

78
.time-grid {
89
--sidebarWidth: 12em;
910
--stepRowHeight: 4em;
1011
--primaryRowHeight: 4em;
1112
position: relative;
13+
margin-right: 1em; // make midnight timestamp visible
1214

1315
.time-grid-header {
1416
box-sizing: border-box;
@@ -65,10 +67,60 @@
6567
display: block;
6668
border-top: 1px solid black;
6769
position: absolute;
68-
bottom: var(--stepRowHeight);
70+
bottom: ~"calc(var(--stepRowHeight) + 3em)"; // 3em .timescale height
6971
right: 0;
7072
left: 0;
7173
}
74+
75+
.timescale {
76+
display: grid;
77+
grid-template-columns: repeat(~"calc(var(--primaryColumns) * var(--timestampsPerDay))", 1fr);
78+
border-left: 1px solid @gray-lighter; // this is required to maintain the grid layout
79+
grid-area: ~"4 / 2 / 4 / 3";
80+
height: 3em;
81+
82+
.ticks {
83+
position: relative;
84+
border-right: 1px solid @gray-lighter;
85+
border-left: 1px solid @gray-lighter;
86+
87+
&:after { // overlaps the unnecessary part of border-left
88+
content: '';
89+
position: absolute;
90+
top: 0.25em;
91+
left: 0;
92+
right: 0;
93+
bottom: 0;
94+
transform: translate(-50%);
95+
background: @body-bg-color;
96+
}
97+
}
98+
99+
.timestamp {
100+
display: flex;
101+
flex-direction: column;
102+
align-items: center;
103+
margin-top: 0.5em;
104+
padding-top: 0.5em;
105+
font-size: .5em;
106+
position: relative;
107+
left: -50%;
108+
line-height: 1;
109+
110+
&.midnight {
111+
left: 50%;
112+
}
113+
114+
> span:last-child {
115+
opacity: 0.5;
116+
}
117+
}
118+
119+
span:nth-last-of-type(2), // last .ticks before .midnight
120+
.midnight {
121+
grid-area: ~"1 / -2 / 1 / -1";
122+
}
123+
}
72124
}
73125
}
74126

@@ -134,6 +186,11 @@
134186
font-size: .75em;
135187
opacity: .8;
136188
}
189+
190+
.timescale .timestamp {
191+
color: @gray-semilight;
192+
background: @body-bg-color;
193+
}
137194
}
138195

139196
.timeline.minimal-layout .empty-notice {

0 commit comments

Comments
 (0)