Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedules: Add timescale and time indicator #292

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions library/Notifications/Widget/TimeGrid/Timescale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Widget\TimeGrid;

use DateTime;
use IntlDateFormatter;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Style;
use Locale;

/**
* Creates a localized timescale for the TimeGrid
*/
class Timescale extends BaseHtmlElement
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class documentation is missing (PHPDoc)

{
use Translation;

protected $tag = 'div';

protected $defaultAttributes = ['class' => 'timescale'];

/** @var int The number of days shown */
protected $days;

/** @var Style */
protected $style;

/**
* Create a new Timescale
*
* @param int $days
* @param Style $style
*/
public function __construct(int $days, Style $style)
{
$this->days = $days;
$this->style = $style;
}

public function assemble(): void
{
switch (true) {
case $this->days === 1:
$timestampPerDay = 24;
break;
case $this->days <= 7:
$timestampPerDay = 3;
break;
case $this->days <= 14:
$timestampPerDay = 2;
break;
default:
$timestampPerDay = 1;
}

$this->style->addFor($this, ['--timestampsPerDay' => $timestampPerDay * 2]); // *2 for .ticks

$dateFormatter = new IntlDateFormatter(
Locale::getDefault(),
IntlDateFormatter::NONE,
IntlDateFormatter::SHORT
);

$timeIntervals = 24 / $timestampPerDay;

$time = new DateTime();
$dayTimestamps = [];
for ($i = 0; $i < $timestampPerDay; $i++) {
// am-pm is separated by non-breaking whitespace
$parts = preg_split('/\s/u', $dateFormatter->format($time->setTime($i * $timeIntervals, 0)));

$stamp = [new HtmlElement('span', null, new Text($parts[0]))];
if (isset($parts[1])) {
$stamp[] = new HtmlElement('span', null, new Text($parts[1]));
}

$dayTimestamps[] = new HtmlElement('span', new Attributes(['class' => 'timestamp']), ...$stamp);
$dayTimestamps[] = new HtmlElement('span', new Attributes(['class' => 'ticks']));
}

$allTimestamps = array_merge(...array_fill(0, $this->days, $dayTimestamps));
// clone is required because $allTimestamps contains references of same object
$allTimestamps[] = (clone $allTimestamps[0])->addAttributes(['class' => 'midnight']); // extra stamp of 12AM

$this->addHtml(...$allTimestamps);
}
}
40 changes: 40 additions & 0 deletions library/Notifications/Widget/Timeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
use Icinga\Module\Notifications\Widget\TimeGrid\DynamicGrid;
use Icinga\Module\Notifications\Widget\TimeGrid\EntryProvider;
use Icinga\Module\Notifications\Widget\TimeGrid\GridStep;
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\MinimalGrid;
use Icinga\Module\Notifications\Widget\Timeline\Rotation;
use IntlDateFormatter;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
Expand All @@ -23,6 +26,7 @@
use ipl\Web\Url;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
use Locale;
use SplObjectStorage;
use Traversable;

Expand Down Expand Up @@ -316,6 +320,42 @@ protected function assemble()
Text::create($this->translate('Result'))
)
);

$dateFormatter = new IntlDateFormatter(
Locale::getDefault(),
IntlDateFormatter::NONE,
IntlDateFormatter::SHORT
);

$now = new DateTime();
$currentTime = new HtmlElement(
'div',
new Attributes(['class' => 'time-hand']),
new HtmlElement(
'div',
new Attributes(['class' => 'now', 'title' => $dateFormatter->format($now)]),
Text::create($this->translate('now'))
)
);

$now = Util::roundToNearestThirtyMinute($now);

$this->getStyle()->addFor($currentTime, [
'--timeStartColumn' =>
$now->format('G') * 2 // 2 columns per hour
+ ($now->format('i') >= 30 ? 1 : 0) // 1 column for the half hour
+ 1 // CSS starts counting columns from 1, not zero
]);

$clock = new HtmlElement(
'div',
new Attributes(['class' => 'clock']),
new HtmlElement('div', new Attributes(['class' => 'current-day']), $currentTime)
);

$this->getGrid()
->addHtml(new Timescale($this->days, $this->getStyle()))
->addHtml($clock);
}

$this->addHtml(
Expand Down
91 changes: 89 additions & 2 deletions public/css/timeline.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
.timeline {
display: flex;
flex-direction: column;
overflow: hidden;

.time-grid {
--sidebarWidth: 12em;
--stepRowHeight: 4em;
--primaryRowHeight: 4em;
position: relative;
margin-right: 1em; // make midnight timestamp visible

.time-grid-header {
box-sizing: border-box;
position: sticky;
z-index: 1;
z-index: 2; // overlap the .clock .time-hand
top: 0;
}

Expand Down Expand Up @@ -47,6 +49,7 @@
.overlay .entry {
margin-top: 1em;
margin-bottom: 1em;
z-index: 2; // overlap the .clock .time-hand

.title {
height: 100%;
Expand All @@ -65,10 +68,82 @@
display: block;
border-top: 1px solid black;
position: absolute;
bottom: var(--stepRowHeight);
bottom: ~"calc(var(--stepRowHeight) + 3em)"; // 3em .timescale height
right: 0;
left: 0;
}

.timescale {
display: grid;
grid-template-columns: repeat(~"calc(var(--primaryColumns) * var(--timestampsPerDay))", 1fr);
border-left: 1px solid @gray-lighter; // this is required to maintain the grid layout
grid-area: ~"4 / 2 / 4 / 3";
height: 3em;

.ticks {
position: relative;
border-right: 1px solid @gray-lighter;
border-left: 1px solid @gray-lighter;

&:after { // overlaps the unnecessary part of border-left
content: '';
position: absolute;
top: 0.25em;
left: -10%;
width: .25em;
height: 100%;
background: @body-bg-color;
}
}

.timestamp {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 0.5em;
padding-top: 0.5em;
font-size: .5em;
position: relative;
left: -50%;

&.midnight {
left: 50%;
}
}

span:nth-last-of-type(2), // last .ticks before .midnight
.midnight {
grid-area: ~"1 / -2 / 1 / -1";
}
}

.clock {
display: grid;
grid-template-columns: repeat(var(--primaryColumns), 1fr);
grid-area: ~"3 / 2 / 4 / 3";
border-top: 1px solid transparent; // left not required, otherwise the .time-hand is not aligned properly

.current-day {
display: grid;
grid-template-columns: repeat(var(--columnsPerStep), 1fr);
grid-area: ~"1 / 1 / 2 / 2";

.time-hand {
grid-area: ~"1 / var(--timeStartColumn) / 2 / calc(var(--timeStartColumn) + 1)";
display: flex;
align-items: flex-end;
width: 1px;
border-left: 1px solid red;
z-index: 1;

.now {
.rounded-corners();
padding: 0 .25em;
transform: translate(-50%, 50%);
}
}
}
}
}
}

Expand Down Expand Up @@ -134,6 +209,18 @@
font-size: .75em;
opacity: .8;
}

.timescale .timestamp {
color: @gray-semilight;
background: @body-bg-color;
}

.clock .now {
background-color: @gray-light;
font-size: 0.75em;
color: red;
.user-select(none);
}
}

.timeline.minimal-layout .empty-notice {
Expand Down
Loading