diff --git a/library/Icingadb/Widget/Detail/CheckStatistics.php b/library/Icingadb/Widget/Detail/CheckStatistics.php
index a3e78e62..d06004e1 100644
--- a/library/Icingadb/Widget/Detail/CheckStatistics.php
+++ b/library/Icingadb/Widget/Detail/CheckStatistics.php
@@ -8,10 +8,13 @@ use Icinga\Date\DateFormatter;
use Icinga\Module\Icingadb\Widget\CheckAttempt;
use Icinga\Module\Icingadb\Widget\EmptyState;
use Icinga\Util\Format;
+use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
-use ipl\Html\Html;
+use ipl\Html\FormattedString;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\Text;
use ipl\Web\Common\Card;
-use ipl\Web\Widget\HorizontalKeyValue;
use ipl\Web\Widget\StateBall;
use ipl\Web\Widget\TimeAgo;
use ipl\Web\Widget\TimeSince;
@@ -20,11 +23,28 @@ use ipl\Web\Widget\VerticalKeyValue;
class CheckStatistics extends Card
{
+ const TOP_LEFT_BUBBLE_FLAG = <<<'SVG'
+
+SVG;
+
+ const TOP_RIGHT_BUBBLE_FLAG = <<<'SVG'
+
+SVG;
+
+
protected $object;
protected $tag = 'div';
- protected $defaultAttributes = ['class' => 'progress-bar check-statistics'];
+ protected $defaultAttributes = ['class' => ['progress-bar', 'check-statistics']];
public function __construct($object)
{
@@ -35,96 +55,155 @@ class CheckStatistics extends Card
{
$hPadding = 10;
$durationScale = 80;
+ $checkInterval = $this->getCheckInterval();
- $timeline = Html::tag('div', ['class' => 'check-timeline timeline']);
-
+ $timeline = new HtmlElement('div', Attributes::create(['class' => ['check-timeline', 'timeline']]));
+ $above = new HtmlElement('ul', Attributes::create(['class' => 'above']));
+ $below = new HtmlElement('ul', Attributes::create(['class' => 'below']));
+ $progressBar = new HtmlElement('div', Attributes::create(['class' => 'bar']));
$overdueBar = null;
- $nextCheckTime = $this->object->state->next_check->getTimestamp();
- $checkInterval = $this->getCheckInterval();
+ $now = time();
+ $executionTime = ($this->object->state->execution_time / 1000) + ($this->object->state->latency / 1000);
+
+ $nextCheckTime = $this->object->state->next_check !== null
+ ? $this->object->state->next_check->getTimestamp()
+ : null;
if ($this->object->state->is_overdue) {
$nextCheckTime = $this->object->state->next_update->getTimestamp();
- $leftNow = $durationScale + $hPadding / 2;
- $overdueScale = ($durationScale / 2) * (time() - $nextCheckTime) / (10 * $checkInterval);
- if ($overdueScale > $durationScale / 2) {
- $overdueScale = $durationScale / 2;
- }
+ $durationScale = 60;
- $durationScale -= $overdueScale;
- $overdueBar = Html::tag('div', [
- 'class' => 'timeline-overlay check-overdue',
- 'style' => sprintf(
- 'left: %F%%; width: %F%%;',
- $hPadding + $durationScale,
- $overdueScale + $hPadding / 2
+ $overdueBar = new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'timeline-overlay']),
+ new HtmlElement('div', Attributes::create(['class' => 'now']))
+ );
+
+ $above->addHtml(new HtmlElement(
+ 'li',
+ Attributes::create(['class' => 'now']),
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'bubble']),
+ new HtmlElement('strong', null, Text::create(t('Now')))
)
- ]);
+ ));
+
+ $this->getAttributes()->add('class', 'check-overdue');
} else {
- $leftNow = $durationScale * (1 - ($nextCheckTime - time()) / $checkInterval);
- if ($leftNow > $durationScale) {
- $leftNow = $durationScale;
+ $progressBar->addHtml(new HtmlElement('div', Attributes::create(['class' => 'now'])));
+ }
+
+ if ($nextCheckTime !== null && $nextCheckTime < $now) {
+ $lastUpdateTime = $nextCheckTime;
+ $nextCheckTime = $this->object->state->next_update->getTimestamp() - $executionTime;
+ $executionEndTime = $lastUpdateTime + $executionTime;
+ } else {
+ $lastUpdateTime = $this->object->state->last_update !== null
+ ? $this->object->state->last_update->getTimestamp() - $executionTime
+ : null;
+ $executionEndTime = $this->object->state->last_update !== null
+ ? $this->object->state->last_update->getTimestamp()
+ : null;
+ }
+
+ if ($this->object->state->is_overdue) {
+ $leftNow = 100;
+ } elseif ($nextCheckTime === null) {
+ $leftNow = 0;
+ } else {
+ $leftNow = 100 * (1 - ($nextCheckTime - time()) / ($nextCheckTime - $lastUpdateTime));
+ if ($leftNow > 100) {
+ $leftNow = 100;
} elseif ($leftNow < 0) {
$leftNow = 0;
}
}
- $above = Html::tag('ul', ['class' => 'above']);
- $now = Html::tag(
- 'li',
- [
- 'class' => 'now positioned',
- 'style' => sprintf('left: %F%%', $hPadding + $leftNow)
- ],
- Html::tag(
- 'div',
- ['class' => 'bubble'],
- Html::tag(
- 'strong',
- t('Now')
- )
- )
- );
- $above->add($now);
+ $progressBar->getAttributes()->add('style', sprintf('width: %s%%', $leftNow));
- $markerLast = Html::tag('div', [
- 'class' => 'marker start',
- 'style' => 'left: ' . $hPadding . '%',
- 'title' => $this->object->state->last_update !== null
- ? DateFormatter::formatDateTime($this->object->state->last_update->getTimestamp())
- : null
- ]);
- $markerNext = Html::tag('div', [
- 'class' => 'marker end',
- 'style' => sprintf('left: %F%%', $hPadding + $durationScale),
+ $leftExecutionEnd = $nextCheckTime !== null ? $durationScale * (
+ 1 - ($nextCheckTime - $executionEndTime) / ($nextCheckTime - $lastUpdateTime)
+ ) : 0;
+
+ $markerLast = new HtmlElement('div', Attributes::create([
+ 'class' => ['highlighted', 'marker', 'left'],
+ 'title' => $lastUpdateTime !== null ? DateFormatter::formatDateTime($lastUpdateTime) : null
+ ]));
+ $markerNext = new HtmlElement('div', Attributes::create([
+ 'class' => ['highlighted', 'marker', 'right'],
'title' => $nextCheckTime !== null ? DateFormatter::formatDateTime($nextCheckTime) : null
- ]);
- $markerNow = Html::tag('div', [
- 'class' => 'marker now',
- 'style' => sprintf('left: %F%%', $hPadding + $leftNow),
- ]);
+ ]));
+ $markerExecutionEnd = new HtmlElement('div', Attributes::create([
+ 'class' => ['highlighted', 'marker'],
+ 'style' => sprintf('left: %F%%', $hPadding + $leftExecutionEnd),
+ ]));
- $timeline->add([
+ $progress = new HtmlElement('div', Attributes::create([
+ 'class' => ['progress', time() < $executionEndTime ? 'running' : null]
+ ]), $progressBar);
+ if ($nextCheckTime !== null) {
+ $progress->addAttributes([
+ 'data-animate-progress' => true,
+ 'data-start-time' => $lastUpdateTime,
+ 'data-end-time' => $nextCheckTime,
+ 'data-switch-after' => $executionTime,
+ 'data-switch-class' => 'running'
+ ]);
+ }
+
+ $timeline->addHtml(
+ $progress,
$markerLast,
- $markerNow,
- $markerNext,
- $overdueBar
- ]);
+ $markerExecutionEnd,
+ $markerNext
+ )->add($overdueBar);
- $lastUpdate = Html::tag(
+ $executionStart = new HtmlElement(
'li',
- ['class' => 'start'],
- Html::tag(
+ Attributes::create(['class' => 'left']),
+ new HtmlElement(
'div',
- ['class' => 'bubble upwards'],
- new VerticalKeyValue(t('Last update'), $this->object->state->last_update !== null
- ? new TimeAgo($this->object->state->last_update->getTimestamp())
- : t('PENDING'))
+ Attributes::create(['class' => ['bubble', 'upwards', 'top-right-aligned']]),
+ new VerticalKeyValue(
+ t('Execution Start'),
+ $lastUpdateTime ? new TimeAgo($lastUpdateTime) : t('PENDING')
+ ),
+ HtmlString::create(self::TOP_RIGHT_BUBBLE_FLAG)
)
);
- $interval = Html::tag(
+ $executionEnd = new HtmlElement(
'li',
- ['class' => 'interval'],
+ Attributes::create([
+ 'class' => 'positioned',
+ 'style' => sprintf('left: %F%%', $hPadding + $leftExecutionEnd)
+ ]),
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => ['bubble', 'upwards', 'top-left-aligned']]),
+ new VerticalKeyValue(
+ t('Execution End'),
+ $executionEndTime !== null
+ ? ($executionEndTime > $now
+ ? new TimeUntil($executionEndTime)
+ : new TimeAgo($executionEndTime))
+ : t('PENDING')
+ ),
+ HtmlString::create(self::TOP_LEFT_BUBBLE_FLAG)
+ )
+ );
+
+ $intervalLine = new HtmlElement(
+ 'li',
+ Attributes::create([
+ 'class' => 'interval-line',
+ 'style' => sprintf(
+ 'left: %F%%; width: %F%%;',
+ $hPadding + $leftExecutionEnd,
+ $durationScale - $leftExecutionEnd
+ )
+ ]),
new VerticalKeyValue(
t('Interval'),
$checkInterval
@@ -132,6 +211,29 @@ class CheckStatistics extends Card
: (new EmptyState(t('n. a.')))->setTag('span')
)
);
+ $executionLine = new HtmlElement(
+ 'li',
+ Attributes::create([
+ 'class' => ['interval-line', 'execution-line'],
+ 'style' => sprintf('left: %F%%; width: %F%%;', $hPadding, $leftExecutionEnd)
+ ]),
+ new VerticalKeyValue(
+ sprintf('%s / %s', t('Execution Time'), t('Latency')),
+ FormattedString::create(
+ '%s / %s',
+ $this->object->state->execution_time !== null
+ ? Format::seconds($this->object->state->execution_time / 1000)
+ : (new EmptyState(t('n. a.')))->setTag('span'),
+ $this->object->state->latency !== null
+ ? Format::seconds($this->object->state->latency / 1000)
+ : (new EmptyState(t('n. a.')))->setTag('span')
+ )
+ )
+ );
+ if ($executionEndTime !== null) {
+ $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'start'])));
+ $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'end'])));
+ }
if ($this->isChecksDisabled()) {
$nextCheckBubbleContent = new VerticalKeyValue(
@@ -145,34 +247,34 @@ class CheckStatistics extends Card
? new VerticalKeyValue(t('Overdue'), new TimeSince($nextCheckTime))
: new VerticalKeyValue(
t('Next Check'),
- $nextCheckTime !== null ? new TimeUntil($nextCheckTime) : t('PENDING')
+ $nextCheckTime !== null
+ ? ($nextCheckTime > $now
+ ? new TimeUntil($nextCheckTime)
+ : new TimeAgo($nextCheckTime))
+ : t('PENDING')
);
}
- $nextCheck = Html::tag(
+ $nextCheck = new HtmlElement(
'li',
- ['class' => 'end'],
- Html::tag(
+ Attributes::create(['class' => 'right']),
+ new HtmlElement(
'div',
- ['class' => 'bubble upwards'],
+ Attributes::create(['class' => ['bubble', 'upwards']]),
$nextCheckBubbleContent
)
);
- $below = Html::tag(
- 'ul',
- [
- 'class' => 'below',
- 'style' => sprintf('width: %F%%;', $durationScale)
- ]
- );
- $below->add([
- $lastUpdate,
- $interval,
- $nextCheck
- ]);
+ $above->addHtml($executionLine);
- $body->add([$above, $timeline, $below]);
+ $below->addHtml(
+ $executionStart,
+ $executionEnd,
+ $intervalLine,
+ $nextCheck
+ );
+
+ $body->addHtml($above, $timeline, $below);
}
/**
@@ -185,23 +287,6 @@ class CheckStatistics extends Card
return ! ($this->object->active_checks_enabled || $this->object->passive_checks_enabled);
}
- protected function assembleFooter(BaseHtmlElement $footer)
- {
- $footer->add(new HorizontalKeyValue(
- t('Scheduling Source') . ':',
- $this->object->state->scheduling_source ?? (new EmptyState(t('n. a.')))->setTag('span')
- ));
-
- if ($this->object->timeperiod->id) {
- $footer->add(new HorizontalKeyValue(
- t('Timeperiod') . ':',
- $this->object->timeperiod->display_name ?? $this->object->timeperiod->name
- ));
-
- $footer->addAttributes(['class' => 'space-between']);
- }
- }
-
protected function assembleHeader(BaseHtmlElement $header)
{
$checkSource = (new EmptyState(t('n. a.')))->setTag('span');
@@ -213,26 +298,28 @@ class CheckStatistics extends Card
];
}
- $header->add([
+ $header->addHtml(
new VerticalKeyValue(t('Command'), $this->object->checkcommand_name),
+ new VerticalKeyValue(
+ t('Scheduling Source'),
+ $this->object->state->scheduling_source ?? (new EmptyState(t('n. a.')))->setTag('span')
+ )
+ );
+
+ if ($this->object->timeperiod->id) {
+ $header->addHtml(new VerticalKeyValue(
+ t('Timeperiod'),
+ $this->object->timeperiod->display_name ?? $this->object->timeperiod->name
+ ));
+ }
+
+ $header->addHtml(
new VerticalKeyValue(
t('Attempts'),
new CheckAttempt((int) $this->object->state->check_attempt, (int) $this->object->max_check_attempts)
),
- new VerticalKeyValue(t('Check Source'), $checkSource),
- new VerticalKeyValue(
- t('Execution time'),
- $this->object->state->execution_time
- ? Format::seconds($this->object->state->execution_time / 1000)
- : (new EmptyState(t('n. a.')))->setTag('span')
- ),
- new VerticalKeyValue(
- t('Latency'),
- $this->object->state->latency
- ? Format::seconds($this->object->state->latency / 1000)
- : (new EmptyState(t('n. a.')))->setTag('span')
- )
- ]);
+ new VerticalKeyValue(t('Check Source'), $checkSource)
+ );
}
/**
@@ -268,13 +355,13 @@ class CheckStatistics extends Card
parent::assemble();
if ($this->isChecksDisabled()) {
- $this->add(Html::tag(
+ $this->addHtml(new HtmlElement(
'div',
- ['class' => 'checks-disabled-overlay'],
- Html::tag(
+ Attributes::create(['class' => 'checks-disabled-overlay']),
+ new HtmlElement(
'strong',
- ['class' => 'notes'],
- t('active and passive checks are disabled')
+ Attributes::create(['class' => 'notes']),
+ Text::create(t('active and passive checks are disabled'))
)
));
}
diff --git a/public/css/mixin/progress-bar.less b/public/css/mixin/progress-bar.less
index 532ca2c3..74bb83b9 100644
--- a/public/css/mixin/progress-bar.less
+++ b/public/css/mixin/progress-bar.less
@@ -1,29 +1,36 @@
.progress-bar() {
&.progress-bar {
+ --hPadding: 10%;
+ --duration-scale: 80%;
+
.above,
.below {
list-style-type: none;
- position: relative;
margin: 0;
- }
+ padding: 0;
- .above {
- padding: ~"calc(1em + 1px) 10%";
+ position: relative;
+ height: ~"calc(2em + 2px)";
}
.below {
- padding: 1.25em 10%;
+ > .left {
+ position: absolute;
+ left: var(--hPadding);
+ top: 0;
+ }
+
+ > .right {
+ position: absolute;
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ top: 0;
+ }
}
.positioned {
position: absolute;
}
- :not(.positioned).end .bubble {
- // to move the center of the bubble to the end of the wrapper (for check-statistics end bubble only).
- transform: translate(50%, 0);
- }
-
.bubble {
.rounded-corners(.25em);
background-color: @body-bg-color;
@@ -48,7 +55,7 @@
z-index: 5;
}
- &:before {
+ &::before {
background-color: @body-bg-color;
border-bottom: 1px solid @gray-light;
border-right: 1px solid @gray-light;
@@ -65,33 +72,33 @@
left: 50%;
}
- &.upwards:before {
+ &.upwards::before {
bottom: auto;
- top: -.5em;
+ top: -7/12em;
transform: rotate(225deg);
}
- &.right {
+ &.right-aligned {
// This is (.675em (:before placement) + .5em (half :before width)) + 1px (:before border)
transform: translate(~"calc(-1.175em - 1px)", 0);
+
+ &::before {
+ top: auto;
+ left: 1.175em;
+ bottom: -.5em;
+ }
}
- &.right:before {
- bottom: auto;
- left: 1.175em;
- top: -.5em;
- }
-
- &.left {
+ &.left-aligned {
// entire width (moves the right border in place of the left) + (.675em (:before placement) + .5em (half :before width)) + 1px (:before border)
transform: translate(~"calc(-100% + 1.175em + 1px)", 0);
- }
- &.left:before {
- bottom: auto;
- left: auto;
- right: .675em;
- top: -.5em;
+ &::before {
+ top: auto;
+ left: auto;
+ right: .675em;
+ bottom: -.5em;
+ }
}
}
@@ -116,6 +123,8 @@
}
.timeline {
+ @marker-gap: 1/12em;
+
.rounded-corners(.5em);
background-color: @gray-lighter;
height: 1em;
@@ -129,50 +138,79 @@
background-color: @gray-light;
height: .857em;
margin-left: -.857/2em;
- margin-top: -1/12em;
width: .857em;
z-index: 2;
position: absolute;
- top: 2/12em;
+ top: @marker-gap;
- &.now {
- background-color: @gray;
- }
-
- &.start,
- &.end {
+ &.highlighted {
background-color: @icinga-blue;
}
+
+ &.left {
+ left: var(--hPadding);
+ }
+
+ &.right {
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ }
}
- }
-
- .timeline-overlay {
- height: 100%;
- opacity: .6;
- position: absolute;
-
- &:before,
- &:after {
- content: "";
- display: block;
- height: 1em;
- width: .5em;
+ .progress {
position: absolute;
- top: 0;
+ left: var(--hPadding);
+ width: var(--duration-scale);
+
+ &::before {
+ content: "";
+ display: block;
+ width: .5em + @marker-gap;
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+
+ .rounded-corners(.5em);
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ position: absolute;
+ left: -.5em - @marker-gap;
+ }
+
+ > .bar {
+ width: 0; // set by progress-bar.js
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+ }
+
+ &::before,
+ > .bar {
+ background-color: @gray-light;
+ }
}
- &:before {
- border-bottom-left-radius: .5em;
- border-top-left-radius: .5em;
- left: -.5em;
+ .timeline-overlay {
+ position: absolute;
+ left: ~"calc(var(--hPadding) + var(--duration-scale))";
+ width: var(--overlay-scale);
+ height: 1em + (@marker-gap * 2);
+ margin-top: -@marker-gap;
+
+ opacity: .6;
}
- &:after {
- border-bottom-right-radius: .5em;
- border-top-right-radius: .5em;
- right: -.5em;
+ .progress > .bar,
+ .timeline-overlay {
+ display: flex;
+ justify-content: flex-end;
+
+ .now {
+ width: .25em;
+
+ border: solid @default-bg;
+ border-width: 1px 0 1px 0;
+ background-color: red;
+ }
}
}
}
diff --git a/public/css/widget/check-statistics.less b/public/css/widget/check-statistics.less
index 47db8185..ee7ba043 100644
--- a/public/css/widget/check-statistics.less
+++ b/public/css/widget/check-statistics.less
@@ -2,68 +2,148 @@
position: relative;
.card();
.progress-bar();
- .card-footer {
- display: flex;
- justify-content: center;
- border-top: 1px solid @gray-light;
-
- &.space-between {
- justify-content: space-between;
- padding: 0 0.5em;
- }
-
- .key {
- width: auto;
- margin-right: .28125em; //calculated width
- font-size: .83333333em;
- }
- }
.check-attempt {
display: inline-flex;
}
- &.progress-bar .below {
- padding: 0;
- margin-left: 10%;
- margin-right: auto;
+ .bubble {
+ &.top-left-aligned,
+ &.top-right-aligned {
+ &::before {
+ display: none;
+ }
- display: flex;
- align-items: center;
- justify-content: space-between;
+ svg {
+ position: absolute;
+ top: -1em;
+ width: 1em;
+ height: 1em;
- &:before {
- background-color: @gray;
- content: "";
- display: block;
- height: .25em;
- width: 100%;
+ .bg {
+ fill: @body-bg-color;
+ }
- position: absolute;
- top: ~"calc(50% - .125em)";
+ .border {
+ fill: @gray-light;
+ }
+ }
+ }
+
+ &.top-left-aligned {
+ transform: unset;
+ border-top-left-radius: 0;
+
+ svg {
+ left: -1px;
+ }
+ }
+
+ &.top-right-aligned {
+ transform: translate(-100%);
+ border-top-right-radius: 0;
+
+ svg {
+ right: -1px;
+ }
}
}
- .interval {
- background-color: @body-bg-color;
- position: relative;
+ // ATTENTION!: `&.progress-bar {` must not be used here, seems to confuse the less parser!!!!111
+
+ &.progress-bar .timeline .progress.running {
+ &::before,
+ > .bar {
+ background: @state-ok;
+ }
}
- .check-overdue {
- background-color: @color-down;
- opacity: 1;
+ &.progress-bar .check-timeline {
+ margin-top: .5em;
+ }
+ &.progress-bar .above {
+ margin-top: .5em;
+ }
- &:before,
- &:after {
- background-color: @color-down;
+ .interval-line {
+ position: absolute;
+ height: 100%;
+
+ &::before {
+ position: absolute;
+ top: ~"calc(50% - .125em)";
+ display: block;
+ height: .25em;
+ width: 100%;
+ content: "";
+
+ background-color: @gray-light;
+ }
+
+ .vertical-key-value {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, 0);
+
+ padding: 0 .2em;
+ background-color: @body-bg-color;
+ }
+
+ .start,
+ .end {
+ position: absolute;
+ top: 50%;
+ width: .25em;
+ height: 1em;
+ background-color: @gray;
+ }
+
+ .start {
+ left: 0;
+ transform: translate(-50%, -50%);
+ }
+
+ .end {
+ right: 0;
+ transform: translate(50%, -50%);
+ }
+ }
+
+ .execution-line .vertical-key-value {
+ z-index: 1;
+ }
+
+ &.check-overdue {
+ --duration-scale: 60%;
+ --overlay-scale: 20%;
+
+ .above {
+ .now {
+ position: absolute;
+ right: var(--hPadding);
+ bottom: 0;
+
+ .bubble {
+ // to move the center of the bubble to the end of the wrapper.
+ transform: translate(50%, 0);
+ }
+ }
+ }
+
+ .timeline-overlay {
+ background: linear-gradient(90deg, @gray-light 0, @color-down 2em);
+ opacity: 1;
+
+ &::after {
+ background-color: @color-down;
+ }
}
}
&.checks-disabled.progress-bar {
.timeline {
.marker {
- &.start,
- &.end {
+ &.highlighted {
background-color: @gray;
}
}