Skip to content

Decouple time widgets#350

Open
Jan-Schuppik wants to merge 66 commits intomainfrom
decouple-time-widgets
Open

Decouple time widgets#350
Jan-Schuppik wants to merge 66 commits intomainfrom
decouple-time-widgets

Conversation

@Jan-Schuppik
Copy link
Copy Markdown

@Jan-Schuppik Jan-Schuppik commented Jan 8, 2026

This PR removes the direct dependency of the time widgets on Icinga Web 2 (\Icinga\Date\DateFormatter::formatDateTime()) and introduces a native relative-time formatting behavior in IPL Web.

Resolve:

Depends on:

Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Copy link
Copy Markdown
Member

@nilmerg nilmerg left a comment

Choose a reason for hiding this comment

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

Didn't look at JS yet.

Also, add tests for Time and its child classes. Test formatting and rendering.

Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/TimeAgo.php Outdated
Comment thread src/Widget/TimeSince.php Outdated
Comment thread src/Widget/TimeUntil.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Copy link
Copy Markdown
Member

@nilmerg nilmerg left a comment

Choose a reason for hiding this comment

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

There are still no tests!

Comment thread src/Widget/Time.php
Comment thread src/Widget/Time.php
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/TimeAgo.php Outdated
Copy link
Copy Markdown
Member

@nilmerg nilmerg left a comment

Choose a reason for hiding this comment

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

I stopped reading the tests once I saw more than one skip due to when the test is performed. Sorry, but this just non-sense. Tests must always succeed.

In addition, they make too heavy usage of contains and regex assertions. They test a HTML widget, so should assert the HTML structure: $obj actual, $html expected. Other tests exactly work like this and there is even a predefined assertion as part of ipl\Tests\Html\TestCase.

So please refactor your tests accordingly.

Comment thread src/Widget/Time.php
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/Time.php Outdated
Comment thread src/Widget/TimeAgo.php Outdated
Comment thread src/Widget/TimeAgo.php Outdated
Comment thread src/Widget/Time.php
Comment thread src/Widget/TimeSince.php
Comment thread src/Widget/TimeUntil.php
Comment thread tests/Widget/TimeAgoTest.php Outdated
Comment thread tests/Widget/TimeTest.php Outdated
@Jan-Schuppik Jan-Schuppik force-pushed the decouple-time-widgets branch 4 times, most recently from 65cf226 to 7add9f3 Compare March 23, 2026 12:48
Copy link
Copy Markdown
Contributor

@BastianLedererIcinga BastianLedererIcinga left a comment

Choose a reason for hiding this comment

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

I see my icingadb-web PR is still not entirely compatible with your changes, since I see no proper way to include a data-ago-label in Time widgets, which is required for the switch from absolute to relative. Let's discuss a solution and this time document the agreed approach in this PR.

Comment thread asset/js/behavior/RelativeTimeBehavior.js Outdated
Comment thread asset/js/widget/RelativeTime.js Outdated
Comment thread asset/js/widget/RelativeTime.js Outdated
Comment thread tests/Widget/TimeAgoTest.php Outdated
Allow the Time widget to accept a custom IntlDateFormatter instance,
enabling locale-aware and customizable time formatting. Falls back to
the default formatted datetime string when no formatter is set.

Also declare the ext-intl PHP extension as an explicit dependency in
composer.json.
PHPStan checks that the values in specified keys have the correct types.
source: https://phpstan.org/writing-php-code/phpdoc-types#array-shapes
Remove the separate assemble() override in TimeAgo, TimeSince and
TimeUntil. Attribute setup and text formatting are now handled entirely
within format(), eliminating the duplicate diff() call and reducing the
number of methods subclasses need to override.
…ocks

Replace \Exception with the imported Exception class across Time and its
subclasses, and add the missing Exception import and @throws annotation
to Time::relative().
Previously, Time::diff() and Time::relative() always used new DateTime()
as the reference point, making deterministic unit tests impossible.

Introduce an optional $compareTime parameter throughout the widget family
so callers can inject an explicit reference time:

- Time::diff() accepts ?DateTime $compareTime instead of computing "now"
  internally; Time::relative() forwards it to TimeAgo/TimeUntil.
- TimeAgo, TimeSince, and TimeUntil store the injected time in the new
  Time::$compareTime property.
- Year/day comparisons using date('Y')/date('d') are replaced by the new
  isSameByFormat() helper, removing the last implicit "now" dependencies.
- data-relative-time attributes are moved to $defaultAttributes, removing
  duplicate code from format() methods.
- Fix inverted prefix logic in TimeUntil: only prepend '-' when invert
  is 0 and the interval is non-zero.
- Move all test classes out of namespace blocks into proper ipl\Tests\Web\Widget namespace
- Extend TestCase instead of PHPUnit\Framework\TestCase directly
- Initialize StaticTranslator with NoopTranslator in setUp() to avoid translation side effects
- Consolidate redundant constructor tests (int/float/DateTime variants) into fewer, clearer cases
- Rename test methods to focus on observable behavior rather than input type (e.g. testConstructorAcceptsIntTimestamp → testConstructorAcceptsTimestamp)
- Add $compareTime parameter usage to pin time in tests for deterministic results
Copy link
Copy Markdown
Member

@nilmerg nilmerg left a comment

Choose a reason for hiding this comment

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

@BastianLedererIcinga Please take over now, as @Jan-Schuppik is already occupied for the coming weeks and I want to finish this soon.

Don't hesitate to let Claude refactor the tests 😉

Comment thread src/Widget/TimeAgo.php Outdated
Comment thread src/Widget/TimeSince.php Outdated
Comment thread src/Widget/TimeUntil.php Outdated
Comment thread composer.json Outdated
Comment thread asset/js/widget/RelativeTime.js Outdated
Comment thread asset/js/widget/RelativeTime.js Outdated
Comment thread asset/js/behavior/RelativeTimeBehavior.js Outdated
Comment thread asset/js/widget/RelativeTime.js Outdated
BastianLedererIcinga and others added 5 commits April 17, 2026 08:39
Add a new test case and fix intentation of heredoc
Scan for new relative time elements on `rendered` and track existing ones.
Use `setInterval` instead of the `icinga.timer`.
Since `Translation` is used in the `Time` class already it can be dropped
from the subclasses.
Tests are adjusted to match the new type of `$compareTime`.
Comment thread src/Widget/TimeAgo.php Outdated
public function __construct(int|float|DateTime|null $time = null, ?DateTime $compareTime = null)
{
if ($compareTime !== null) {
$this->compareTime = $this->castToDateTime($compareTime);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It doesn't need to be cast anymore ;)

Comment thread src/Widget/TimeSince.php Outdated
public function __construct(int|float|DateTime|null $time = null, ?DateTime $compareTime = null)
{
if ($compareTime !== null) {
$this->compareTime = $this->castToDateTime($compareTime);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here, no cast necessary

Comment thread src/Widget/TimeUntil.php Outdated
public function __construct(int|float|DateTime|null $time = null, ?DateTime $compareTime = null)
{
if ($compareTime !== null) {
$this->compareTime = $this->castToDateTime($compareTime);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and here, you know the deal

* @private
*/
this._relativeTime = new RelativeTime(icinga.config.timezone);
this._relativeTime.scan(document);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe it shouldn't be necessary to scan upon construction. The render event is triggered upon page load by Icinga Web.

}

onTimeRendered(event) {
event.data.self._relativeTime.scan(event.target);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please only scan in case event.currentTarget === event.target. Otherwise this will scan containers multiple times while the event bubbles up to the ancestors.

For this to reliably work the listener registration in the constructor needs to use .container as condition.

Comment thread asset/js/RelativeTime.js
tick() {
this._trackedElements.forEach((ref) => {
const el = ref.deref();
if (el) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you only add to _trackedElements, drop dead refs from the set to avoid leaking memory.

Comment thread asset/js/RelativeTime.js Outdated
}

updateElement(element) {
const DYNAMIC_RELATIVE_TIME_THRESHOLD = 60 * 60;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please make this a class constant/property

Comment thread asset/js/RelativeTime.js Outdated

_getTimeDifferenceInSeconds(element, future = false) {

const fromDateTimeWithTimezone = (element, future = false) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this a separate function?

Comment thread asset/js/RelativeTime.js Outdated
}
}

_getTimeDifferenceInSeconds(element, future = false) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please remove underscores from all methods

Comment thread asset/js/RelativeTime.js Outdated
}

_getOffset() {
if (!this._offsetCache) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

a space after the negation operator please

@nilmerg nilmerg added this to the 0.14.0 milestone Apr 17, 2026
- Make `DYNAMIC_RELATIVE_TIME_THRESHOLD` a class property
- Remove underscores from method names
- Simplify `getTimeDifferenceInSeconds`
- Add missing space after negation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants