From 48cb52209e4edcfeb8346310d50f8c2f641409e2 Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 11 Feb 2025 13:54:51 +0100 Subject: [PATCH 1/2] Schedule detail: Introduce Timescale --- .../Widget/TimeGrid/Timescale.php | 93 +++++++++++++++++++ library/Notifications/Widget/Timeline.php | 4 + public/css/timeline.less | 53 ++++++++++- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 library/Notifications/Widget/TimeGrid/Timescale.php diff --git a/library/Notifications/Widget/TimeGrid/Timescale.php b/library/Notifications/Widget/TimeGrid/Timescale.php new file mode 100644 index 00000000..f53fbef8 --- /dev/null +++ b/library/Notifications/Widget/TimeGrid/Timescale.php @@ -0,0 +1,93 @@ + '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); + } +} diff --git a/library/Notifications/Widget/Timeline.php b/library/Notifications/Widget/Timeline.php index a9a41f9d..bb1e9028 100644 --- a/library/Notifications/Widget/Timeline.php +++ b/library/Notifications/Widget/Timeline.php @@ -11,6 +11,7 @@ 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\Timeline\Entry; use Icinga\Module\Notifications\Widget\Timeline\MinimalGrid; use Icinga\Module\Notifications\Widget\Timeline\Rotation; @@ -316,6 +317,9 @@ protected function assemble() Text::create($this->translate('Result')) ) ); + + $this->getGrid() + ->addHtml(new Timescale($this->days, $this->getStyle())); } $this->addHtml( diff --git a/public/css/timeline.less b/public/css/timeline.less index f92c0411..84fec062 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -3,12 +3,14 @@ .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; @@ -65,10 +67,54 @@ 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"; + } + } } } @@ -134,6 +180,11 @@ font-size: .75em; opacity: .8; } + + .timescale .timestamp { + color: @gray-semilight; + background: @body-bg-color; + } } .timeline.minimal-layout .empty-notice { From 66923ce20b04656937c160257225837663961fbc Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Tue, 11 Feb 2025 13:55:44 +0100 Subject: [PATCH 2/2] Schedule detail: Introduce current time indicator --- library/Notifications/Widget/Timeline.php | 38 ++++++++++++++++++++++- public/css/timeline.less | 38 ++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/library/Notifications/Widget/Timeline.php b/library/Notifications/Widget/Timeline.php index bb1e9028..3d7ccaa3 100644 --- a/library/Notifications/Widget/Timeline.php +++ b/library/Notifications/Widget/Timeline.php @@ -12,9 +12,11 @@ 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; @@ -24,6 +26,7 @@ use ipl\Web\Url; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; +use Locale; use SplObjectStorage; use Traversable; @@ -318,8 +321,41 @@ protected function assemble() ) ); + $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(new Timescale($this->days, $this->getStyle())) + ->addHtml($clock); } $this->addHtml( diff --git a/public/css/timeline.less b/public/css/timeline.less index 84fec062..84cfb040 100644 --- a/public/css/timeline.less +++ b/public/css/timeline.less @@ -15,7 +15,7 @@ .time-grid-header { box-sizing: border-box; position: sticky; - z-index: 1; + z-index: 2; // overlap the .clock .time-hand top: 0; } @@ -49,6 +49,7 @@ .overlay .entry { margin-top: 1em; margin-bottom: 1em; + z-index: 2; // overlap the .clock .time-hand .title { height: 100%; @@ -115,6 +116,34 @@ 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%); + } + } + } + } } } @@ -185,6 +214,13 @@ 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 {