diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
commit | b18bc644404e02b57635bfcc8258e85abb141146 (patch) | |
tree | 686512eacb2dba0055277ef7ec2f28695b3418ea /library/Icingadb/Widget | |
parent | Initial commit. (diff) | |
download | icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.tar.xz icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.zip |
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
96 files changed, 8561 insertions, 0 deletions
diff --git a/library/Icingadb/Widget/AttemptBall.php b/library/Icingadb/Widget/AttemptBall.php new file mode 100644 index 0000000..e57c59c --- /dev/null +++ b/library/Icingadb/Widget/AttemptBall.php @@ -0,0 +1,31 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; + +/** + * Visually represents one single check attempt. + */ +class AttemptBall extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'ball']; + + /** + * Create a new attempt ball + * + * @param bool $taken Whether the attempt was taken + */ + public function __construct(bool $taken = false) + { + if ($taken) { + $this->addAttributes(['class' => 'ball-size-s taken']); + } else { + $this->addAttributes(['class' => 'ball-size-xs']); + } + } +} diff --git a/library/Icingadb/Widget/CheckAttempt.php b/library/Icingadb/Widget/CheckAttempt.php new file mode 100644 index 0000000..cf12de3 --- /dev/null +++ b/library/Icingadb/Widget/CheckAttempt.php @@ -0,0 +1,54 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormattedString; + +/** + * Visually represents the check attempts taken out of max check attempts. + */ +class CheckAttempt extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'check-attempt']; + + /** @var int Current attempt */ + protected $attempt; + + /** @var int Max check attempts */ + protected $maxAttempts; + + /** + * Create a new check attempt widget + * + * @param int $attempt Current check attempt + * @param int $maxAttempts Max check attempts + */ + public function __construct(int $attempt, int $maxAttempts) + { + $this->attempt = $attempt; + $this->maxAttempts = $maxAttempts; + } + + protected function assemble() + { + if ($this->attempt == $this->maxAttempts) { + return; + } + + if ($this->maxAttempts > 5) { + $this->add(FormattedString::create('%d/%d', $this->attempt, $this->maxAttempts)); + } else { + for ($i = 0; $i < $this->attempt; ++$i) { + $this->add(new AttemptBall(true)); + } + for ($i = $this->attempt; $i < $this->maxAttempts; ++$i) { + $this->add(new AttemptBall()); + } + } + } +} diff --git a/library/Icingadb/Widget/Detail/CheckStatistics.php b/library/Icingadb/Widget/Detail/CheckStatistics.php new file mode 100644 index 0000000..8a826d5 --- /dev/null +++ b/library/Icingadb/Widget/Detail/CheckStatistics.php @@ -0,0 +1,373 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Widget\CheckAttempt; +use Icinga\Util\Format; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +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\EmptyState; +use ipl\Web\Compat\StyleWithNonce; +use ipl\Web\Widget\TimeAgo; +use ipl\Web\Widget\TimeSince; +use ipl\Web\Widget\TimeUntil; +use ipl\Web\Widget\VerticalKeyValue; + +class CheckStatistics extends Card +{ + const TOP_LEFT_BUBBLE_FLAG = <<<'SVG' +<svg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'> + <path class='bg' d='M0 0L13 13L3.15334e-06 13L0 0Z'/> + <path class='border' fill-rule='evenodd' clip-rule='evenodd' + d='M0 0L3.3959e-06 14L14 14L0 0ZM1 2.41421L1 13L11.5858 13L1 2.41421Z'/> +</svg> +SVG; + + const TOP_RIGHT_BUBBLE_FLAG = <<<'SVG' +<svg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'> + <path class='bg' d="M12 0L-1 13L12 13L12 0Z"/> + <path class='border' fill-rule="evenodd" clip-rule="evenodd" + d="M12 0L12 14L-2 14L12 0ZM11 2.41421L11 13L0.414213 13L11 2.41421Z"/> +</svg> +SVG; + + protected $object; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => ['progress-bar', 'check-statistics']]; + + public function __construct($object) + { + $this->object = $object; + } + + protected function assembleBody(BaseHtmlElement $body) + { + $styleElement = (new StyleWithNonce()) + ->setModule('icingadb'); + + $hPadding = 10; + $durationScale = 80; + $checkInterval = $this->getCheckInterval(); + + $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; + + $now = time(); + $executionTime = ($this->object->state->execution_time / 1000) + ($this->object->state->latency / 1000); + + $nextCheckTime = $this->object->state->next_check !== null && ! $this->isChecksDisabled() + ? $this->object->state->next_check->getTimestamp() + : null; + if ($this->object->state->is_overdue) { + $nextCheckTime = $this->object->state->next_update->getTimestamp(); + + $durationScale = 60; + + $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 { + $progressBar->addHtml(new HtmlElement('div', Attributes::create(['class' => 'now']))); + } + + if ($nextCheckTime !== null && ! $this->object->state->is_overdue && $nextCheckTime < $now) { + // If the next check is already in the past but not overdue, it means the check is probably running. + // Icinga only updates the state once the check reports a result, that's why we have to simulate the + // execution start and end time, as well as the next check time. + $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; + } elseif (! $this->object->state->is_reachable && time() - $executionEndTime > $checkInterval * 2) { + // We have no way of knowing whether the dependency pauses check scheduling. + // The only way to detect this, is to measure how old the last update is. + $nextCheckTime = null; + $leftNow = 0; + } elseif ($nextCheckTime - $lastUpdateTime <= 0) { + $leftNow = 0; + } else { + $leftNow = 100 * (1 - ($nextCheckTime - time()) / ($nextCheckTime - $lastUpdateTime)); + if ($leftNow > 100) { + $leftNow = 100; + } elseif ($leftNow < 0) { + $leftNow = 0; + } + } + + $styleElement->addFor($progressBar, ['width' => sprintf('%F%%', $leftNow)]); + + $leftExecutionEnd = $nextCheckTime !== null && $nextCheckTime - $lastUpdateTime > 0 ? $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 + ])); + $markerExecutionEnd = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); + $styleElement->addFor($markerExecutionEnd, [ + 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd) + ]); + + $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, + $markerExecutionEnd, + $markerNext + )->add($overdueBar); + + $executionStart = new HtmlElement( + 'li', + Attributes::create(['class' => 'left']), + new HtmlElement( + 'div', + 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) + ) + ); + $executionEnd = new HtmlElement( + 'li', + Attributes::create(['class' => 'positioned']), + 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) + ) + ); + + $styleElement->addFor($executionEnd, ['left' => sprintf('%F%%', $hPadding + $leftExecutionEnd)]); + + $intervalLine = new HtmlElement( + 'li', + Attributes::create(['class' => 'interval-line']), + new VerticalKeyValue(t('Interval'), Format::seconds($checkInterval)) + ); + + $styleElement->addFor($intervalLine, [ + 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd), + 'width' => sprintf('%F%%', $durationScale - $leftExecutionEnd) + ]); + + $executionLine = new HtmlElement( + 'li', + Attributes::create(['class' => ['interval-line', 'execution-line']]), + 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') + ) + ) + ); + + $styleElement->addFor($executionLine, [ + 'left' => sprintf('%F%%', $hPadding), + 'width' => sprintf('%F%%', $leftExecutionEnd) + ]); + + 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( + t('Next Check'), + t('n.a') + ); + + $this->addAttributes(['class' => 'checks-disabled']); + } else { + $nextCheckBubbleContent = $this->object->state->is_overdue + ? new VerticalKeyValue(t('Overdue'), new TimeSince($nextCheckTime)) + : new VerticalKeyValue( + t('Next Check'), + $nextCheckTime !== null + ? ($nextCheckTime > $now + ? new TimeUntil($nextCheckTime) + : new TimeAgo($nextCheckTime)) + : t('PENDING') + ); + } + + $nextCheck = new HtmlElement( + 'li', + Attributes::create(['class' => 'right']), + new HtmlElement( + 'div', + Attributes::create(['class' => ['bubble', 'upwards']]), + $nextCheckBubbleContent + ) + ); + + $above->addHtml($executionLine); + + $below->addHtml( + $executionStart, + $executionEnd, + $intervalLine, + $nextCheck + ); + + $body->addHtml($above, $timeline, $below, $styleElement); + } + + /** + * Checks if both active and passive checks are disabled + * + * @return bool + */ + protected function isChecksDisabled(): bool + { + return ! ($this->object->active_checks_enabled || $this->object->passive_checks_enabled); + } + + protected function assembleHeader(BaseHtmlElement $header) + { + $checkSource = (new EmptyState(t('n. a.')))->setTag('span'); + if ($this->object->state->check_source) { + $checkSource = Text::create($this->object->state->check_source); + } + + $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) + ); + } + + /** + * Get the active `check_interval` OR `check_retry_interval` + * + * @return int + */ + protected function getCheckInterval(): int + { + if (! ($this->object->state->is_problem && $this->object->state->state_type === 'soft')) { + return $this->object->check_interval; + } + + $delay = ($this->object->state->execution_time + $this->object->state->latency) / 1000 + 5; + $interval = $this->object->state->next_check->getTimestamp() + - $this->object->state->last_update->getTimestamp(); + + // In case passive check is used, the check_retry_interval has no effect. + // Since there is no flag in the database to check if the passive check was triggered. + // We have to manually check if the check_retry_interval matches the calculated interval. + if ( + $this->object->check_retry_interval - $delay <= $interval + && $this->object->check_retry_interval + $delay >= $interval + ) { + return $this->object->check_retry_interval; + } + + return $this->object->check_interval; + } + + protected function assemble() + { + parent::assemble(); + + if ($this->isChecksDisabled()) { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'checks-disabled-overlay']), + new HtmlElement( + 'strong', + Attributes::create(['class' => 'notes']), + Text::create(t('active and passive checks are disabled')) + ) + )); + } + } +} diff --git a/library/Icingadb/Widget/Detail/CommentDetail.php b/library/Icingadb/Widget/Detail/CommentDetail.php new file mode 100644 index 0000000..5b0923e --- /dev/null +++ b/library/Icingadb/Widget/Detail/CommentDetail.php @@ -0,0 +1,140 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Model\Comment; +use Icinga\Module\Icingadb\Widget\MarkdownText; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; +use ipl\Stdlib\Filter; +use ipl\Web\Filter\QueryString; +use ipl\Web\Widget\HorizontalKeyValue; +use ipl\Web\Widget\StateBall; +use ipl\Web\Widget\TimeUntil; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class CommentDetail extends BaseHtmlElement +{ + use Auth; + use TicketLinks; + + protected $comment; + + protected $defaultAttributes = ['class' => ['object-detail', 'comment-detail']]; + + protected $tag = 'div'; + + public function __construct(Comment $comment) + { + $this->comment = $comment; + } + + protected function createComment(): array + { + return [ + Html::tag('h2', t('Comment')), + new MarkdownText($this->createTicketLinks($this->comment->text)) + ]; + } + + protected function createDetails(): array + { + $details = []; + + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + if ($this->comment->object_type === 'host') { + $details[] = new HorizontalKeyValue(t('Host'), [ + $this->comment->host->name, + ' ', + new StateBall($this->comment->host->state->getStateText()) + ]); + } else { + $details[] = new HorizontalKeyValue(t('Service'), Html::sprintf( + t('%s on %s', '<service> on <host>'), + [$this->comment->service->name, ' ', new StateBall($this->comment->service->state->getStateText())], + $this->comment->host->name + )); + } + + $details[] = new HorizontalKeyValue(t('Author'), $this->comment->author); + $details[] = new HorizontalKeyValue( + t('Acknowledgement'), + $this->comment->entry_type === 'ack' ? t('Yes') : t('No') + ); + $details[] = new HorizontalKeyValue( + t('Persistent'), + $this->comment->is_persistent ? t('Yes') : t('No') + ); + $details[] = new HorizontalKeyValue( + t('Created'), + DateFormatter::formatDateTime($this->comment->entry_time->getTimestamp()) + ); + $details[] = new HorizontalKeyValue(t('Expires'), $this->comment->expire_time !== null + ? DateFormatter::formatDateTime($this->comment->expire_time->getTimestamp()) + : t('Never')); + } else { + if ($this->comment->expire_time !== null) { + $details[] = Html::tag( + 'p', + Html::sprintf( + $this->comment->entry_type === 'ack' + ? t('This acknowledgement expires %s.', '..<time-until>') + : t('This comment expires %s.', '..<time-until>'), + new TimeUntil($this->comment->expire_time->getTimestamp()) + ) + ); + } + + if ($this->comment->is_sticky) { + $details[] = Html::tag('p', t('This acknowledgement is sticky.')); + } + } + + if (! empty($details)) { + array_unshift($details, Html::tag('h2', t('Details'))); + } + + return $details; + } + + protected function createRemoveCommentForm() + { + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + return null; + } + + $action = Links::commentsDelete(); + $action->setFilter(Filter::equal('name', $this->comment->name)); + + return (new DeleteCommentForm()) + ->setObjects([$this->comment]) + ->populate(['redirect' => '__BACK__']) + ->setAction($action->getAbsoluteUrl()); + } + + protected function assemble() + { + $this->add($this->createComment()); + + $details = $this->createDetails(); + + if (! empty($details)) { + $this->add($details); + } + + if ( + $this->isGrantedOn( + 'icingadb/command/comment/delete', + $this->comment->{$this->comment->object_type} + ) + ) { + $this->add($this->createRemoveCommentForm()); + } + } +} diff --git a/library/Icingadb/Widget/Detail/CustomVarTable.php b/library/Icingadb/Widget/Detail/CustomVarTable.php new file mode 100644 index 0000000..9d6916b --- /dev/null +++ b/library/Icingadb/Widget/Detail/CustomVarTable.php @@ -0,0 +1,268 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Hook\CustomVarRendererHook; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Orm\Model; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\Icon; +use Closure; + +class CustomVarTable extends BaseHtmlElement +{ + /** @var array The variables */ + protected $data; + + /** @var ?Model The object the variables are bound to */ + protected $object; + + /** @var Closure Callback to apply hooks */ + protected $hookApplier; + + /** @var array The groups as identified by hooks */ + protected $groups = []; + + /** @var string Header title */ + protected $headerTitle; + + /** @var int The nesting level */ + protected $level = 0; + + protected $tag = 'table'; + + /** @var HtmlElement The table body */ + protected $body; + + protected $defaultAttributes = [ + 'class' => ['custom-var-table', 'name-value-table'] + ]; + + /** + * Create a new CustomVarTable + * + * @param iterable $data + * @param ?Model $object + */ + public function __construct($data, Model $object = null) + { + $this->data = $data; + $this->object = $object; + $this->body = new HtmlElement('tbody'); + } + + /** + * Set the header to show + * + * @param string $title + * + * @return $this + */ + protected function setHeader(string $title): self + { + $this->headerTitle = $title; + + return $this; + } + + /** + * Add a new row to the body + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function addRow($name, $value) + { + $this->body->addHtml(new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->level}"]), + new HtmlElement('th', null, Html::wantHtml($name)), + new HtmlElement('td', null, Html::wantHtml($value)) + )); + } + + /** + * Render a variable + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function renderVar($name, $value) + { + if ($this->object !== null && $this->level === 0) { + list($name, $value, $group) = call_user_func($this->hookApplier, $name, $value); + if ($group !== null) { + $this->groups[$group][] = [$name, $value]; + return; + } + } + + $isArray = is_array($value); + switch (true) { + case $isArray && is_int(key($value)): + $this->renderArray($name, $value); + break; + case $isArray: + $this->renderObject($name, $value); + break; + default: + $this->renderScalar($name, $value); + } + } + + /** + * Render an array + * + * @param string $name + * @param array $array + * + * @return void + */ + protected function renderArray($name, array $array) + { + $numItems = count($array); + $name = (new HtmlDocument())->addHtml( + Html::wantHtml($name), + Text::create(' (Array)') + ); + + $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); + + ++$this->level; + + ksort($array); + foreach ($array as $key => $value) { + $this->renderVar("[$key]", $value); + } + + --$this->level; + } + + /** + * Render an object (associative array) + * + * @param mixed $name + * @param array $object + * + * @return void + */ + protected function renderObject($name, array $object) + { + $numItems = count($object); + $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); + + ++$this->level; + + ksort($object); + foreach ($object as $key => $value) { + $this->renderVar($key, $value); + } + + --$this->level; + } + + /** + * Render a scalar + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function renderScalar($name, $value) + { + if ($value === '') { + $value = new EmptyState(t('empty string')); + } + + $this->addRow($name, $value); + } + + /** + * Render a group + * + * @param string $name + * @param iterable $entries + * + * @return void + */ + protected function renderGroup(string $name, $entries) + { + $table = new self($entries); + + $wrapper = $this->getWrapper(); + if ($wrapper === null) { + $wrapper = new HtmlDocument(); + $wrapper->addHtml($this); + $this->prependWrapper($wrapper); + } + + $wrapper->addHtml($table->setHeader($name)); + } + + protected function assemble() + { + if ($this->object !== null) { + $this->hookApplier = CustomVarRendererHook::prepareForObject($this->object); + } + + if ($this->headerTitle !== null) { + $this->getAttributes() + ->add('class', 'collapsible') + ->add('data-visible-height', 100) + ->add('data-toggle-element', 'thead') + ->add( + 'id', + preg_replace('/\s+/', '-', strtolower($this->headerTitle)) . '-customvars' + ); + + $this->addHtml(new HtmlElement('thead', null, new HtmlElement( + 'tr', + null, + new HtmlElement( + 'th', + Attributes::create(['colspan' => 2]), + new HtmlElement( + 'span', + null, + new Icon('angle-right'), + new Icon('angle-down') + ), + Text::create($this->headerTitle) + ) + ))); + } + + if (is_array($this->data)) { + ksort($this->data); + } + + foreach ($this->data as $name => $value) { + $this->renderVar($name, $value); + } + + $this->addHtml($this->body); + + // Hooks can return objects as replacement for keys, hence a generator is needed for group entries + $genGenerator = function ($entries) { + foreach ($entries as list($key, $value)) { + yield $key => $value; + } + }; + + foreach ($this->groups as $group => $entries) { + $this->renderGroup($group, $genGenerator($entries)); + } + } +} diff --git a/library/Icingadb/Widget/Detail/DowntimeCard.php b/library/Icingadb/Widget/Detail/DowntimeCard.php new file mode 100644 index 0000000..81f59da --- /dev/null +++ b/library/Icingadb/Widget/Detail/DowntimeCard.php @@ -0,0 +1,258 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Model\Downtime; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Web\Compat\StyleWithNonce; +use ipl\Web\Widget\TimeAgo; +use ipl\Web\Widget\TimeUntil; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class DowntimeCard extends BaseHtmlElement +{ + protected $downtime; + + protected $duration; + + protected $defaultAttributes = ['class' => 'progress-bar downtime-progress']; + + protected $tag = 'div'; + + protected $start; + + protected $end; + + public function __construct(Downtime $downtime) + { + $this->downtime = $downtime; + + $this->start = $this->downtime->scheduled_start_time->getTimestamp(); + $this->end = $this->downtime->scheduled_end_time->getTimestamp(); + + if ($this->downtime->end_time > $this->downtime->scheduled_end_time) { + $this->duration = $this->downtime->end_time->getTimestamp() - $this->start; + } else { + $this->duration = $this->end - $this->start; + } + } + + protected function assemble() + { + $styleElement = (new StyleWithNonce()) + ->setModule('icingadb'); + + $timeline = Html::tag('div', ['class' => 'downtime-timeline timeline']); + $hPadding = 10; + + $above = Html::tag('ul', ['class' => 'above']); + $below = Html::tag('ul', ['class' => 'below']); + + $markerStart = new HtmlElement('div', Attributes::create(['class' => ['marker' , 'left']])); + $markerEnd = new HtmlElement('div', Attributes::create(['class' => ['marker', 'right']])); + + $timelineProgress = null; + $flexProgress = null; + $markerFlexStart = null; + $markerFlexEnd = null; + + if ($this->end < time()) { + $endTime = new TimeAgo($this->end); + } else { + $endTime = new TimeUntil($this->end); + } + + if ($this->downtime->is_flexible && $this->downtime->is_in_effect) { + $this->addAttributes(['class' => 'flexible in-effect']); + + $flexStartLeft = $hPadding + $this->calcRelativeLeft($this->downtime->start_time->getTimestamp()); + $flexEndLeft = $hPadding + $this->calcRelativeLeft($this->downtime->end_time->getTimestamp()); + + $evade = false; + if ($flexEndLeft - $flexStartLeft < 2) { + $flexStartLeft -= 1; + $flexEndLeft += 1; + + if ($flexEndLeft > $hPadding + $this->calcRelativeLeft($this->end)) { + $flexEndLeft = $hPadding + $this->calcRelativeLeft($this->end) - .5; + $flexStartLeft = $flexEndLeft - 2; + } + + if ($flexStartLeft < $hPadding + $this->calcRelativeLeft($this->start)) { + $flexStartLeft = $hPadding + $this->calcRelativeLeft($this->start) + .5; + $flexEndLeft = $flexStartLeft + 2; + } + + $evade = true; + } + + $markerFlexStart = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); + $markerFlexEnd = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); + + $styleElement + ->addFor($markerFlexStart, ['left' => sprintf('%F%%', $flexStartLeft)]) + ->addFor($markerFlexEnd, ['left' => sprintf('%F%%', $flexEndLeft)]); + + $scheduledEndBubble = new HtmlElement( + 'li', + null, + new HtmlElement( + 'div', + Attributes::create(['class' => ['bubble', 'upwards']]), + new VerticalKeyValue(t('Scheduled End'), $endTime) + ) + ); + + $timelineProgress = new HtmlElement('div', Attributes::create([ + 'class' => ['progress', 'downtime-elapsed'], + 'data-animate-progress' => true, + 'data-start-time' => ((float) $this->downtime->start_time->format('U.u')), + 'data-end-time' => ((float) $this->downtime->end_time->format('U.u')) + ]), new HtmlElement( + 'div', + Attributes::create(['class' => 'bar']), + new HtmlElement('div', Attributes::create(['class' => 'now'])) + )); + + $styleElement->addFor($timelineProgress, [ + 'left' => sprintf('%F%%', $flexStartLeft), + 'width' => sprintf('%F%%', $flexEndLeft - $flexStartLeft) + ]); + + if (time() > $this->end) { + $styleElement + ->addFor($markerEnd, [ + 'left' => sprintf('%F%%', $hPadding + $this->calcRelativeLeft($this->end)) + ]) + ->addFor($scheduledEndBubble, [ + 'left' => sprintf('%F%%', $hPadding + $this->calcRelativeLeft($this->end)) + ]); + } else { + $scheduledEndBubble->getAttributes() + ->add('class', 'right'); + } + + $below->add([ + Html::tag( + 'li', + ['class' => 'left'], + Html::tag( + 'div', + ['class' => ['bubble', 'upwards']], + new VerticalKeyValue(t('Scheduled Start'), new TimeAgo($this->start)) + ) + ), + $scheduledEndBubble + ]); + + $aboveStart = Html::tag('li', ['class' => 'positioned'], Html::tag( + 'div', + ['class' => ['bubble', ($evade ? 'left-aligned' : null)]], + new VerticalKeyValue(t('Start'), new TimeAgo($this->downtime->start_time->getTimestamp())) + )); + + $aboveEnd = Html::tag('li', ['class' => 'positioned'], Html::tag( + 'div', + ['class' => ['bubble', ($evade ? 'right-aligned' : null)]], + new VerticalKeyValue(t('End'), new TimeUntil($this->downtime->end_time->getTimestamp())) + )); + + $styleElement + ->addFor($aboveStart, ['left' => sprintf('%F%%', $flexStartLeft)]) + ->addFor($aboveEnd, ['left' => sprintf('%F%%', $flexEndLeft)]); + + $above->add([$aboveStart, $aboveEnd, $styleElement]); + } elseif ($this->downtime->is_flexible) { + $this->addAttributes(['class' => 'flexible']); + + $below->add([ + Html::tag( + 'li', + ['class' => 'left'], + Html::tag( + 'div', + ['class' => ['bubble', 'upwards']], + new VerticalKeyValue( + t('Scheduled Start'), + time() > $this->start + ? new TimeAgo($this->start) + : new TimeUntil($this->start) + ) + ) + ), + Html::tag( + 'li', + ['class' => 'right'], + Html::tag( + 'div', + ['class' => ['bubble', 'upwards']], + new VerticalKeyValue(t('Scheduled End'), $endTime) + ) + ) + ]); + + $above = null; + } else { + if (time() >= $this->start) { + $timelineProgress = new HtmlElement('div', Attributes::create([ + 'class' => ['progress', 'downtime-elapsed'], + 'data-animate-progress' => true, + 'data-start-time' => $this->start, + 'data-end-time' => $this->end + ]), new HtmlElement( + 'div', + Attributes::create(['class' => 'bar']), + new HtmlElement('div', Attributes::create(['class' => 'now'])) + )); + } + + $below->add([ + Html::tag( + 'li', + ['class' => 'left'], + Html::tag( + 'div', + ['class' => 'bubble upwards'], + new VerticalKeyValue(t('Start'), new TimeAgo($this->start)) + ) + ), + Html::tag( + 'li', + ['class' => 'right'], + Html::tag( + 'div', + ['class' => 'bubble upwards'], + new VerticalKeyValue(t('End'), new TimeUntil($this->end)) + ) + ) + ]); + + $above = null; + } + + $timeline->add([ + $timelineProgress, + $flexProgress, + $markerStart, + $markerEnd, + $markerFlexStart, + $markerFlexEnd + ]); + + $this->add([ + $above, + $timeline, + $below + ]); + } + + protected function calcRelativeLeft($value) + { + return round(($value - $this->start) / $this->duration * 80, 2); + } +} diff --git a/library/Icingadb/Widget/Detail/DowntimeDetail.php b/library/Icingadb/Widget/Detail/DowntimeDetail.php new file mode 100644 index 0000000..9e50f7f --- /dev/null +++ b/library/Icingadb/Widget/Detail/DowntimeDetail.php @@ -0,0 +1,206 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Date\DateFormatter as WebDateFormatter; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Widget\MarkdownText; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm; +use Icinga\Module\Icingadb\Model\Downtime; +use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList; +use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Filter\QueryString; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\StateBall; + +class DowntimeDetail extends BaseHtmlElement +{ + use Auth; + use Database; + use HostLink; + use ServiceLink; + + /** @var BaseHtmlElement */ + protected $control; + + /** @var Downtime */ + protected $downtime; + + protected $defaultAttributes = ['class' => ['object-detail', 'downtime-detail']]; + + protected $tag = 'div'; + + public function __construct(Downtime $downtime) + { + $this->downtime = $downtime; + } + + protected function createCancelDowntimeForm() + { + $action = Links::downtimesDelete(); + $action->setFilter(Filter::equal('name', $this->downtime->name)); + + return (new DeleteDowntimeForm()) + ->setObjects([$this->downtime]) + ->populate(['redirect' => '__BACK__']) + ->setAction($action->getAbsoluteUrl()); + } + + protected function createTimeline(): DowntimeCard + { + return new DowntimeCard($this->downtime); + } + + protected function assemble() + { + $this->add(Html::tag('h2', t('Comment'))); + $this->add(Html::tag('div', [ + new Icon('user'), + Html::sprintf( + t('%s commented: %s', '<username> ..: <comment>'), + $this->downtime->author, + new MarkdownText($this->downtime->comment) + ) + ])); + + $this->add(Html::tag('h2', t('Details'))); + + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + $this->addHtml(new HorizontalKeyValue( + t('Type'), + $this->downtime->is_flexible ? t('Flexible') : t('Fixed') + )); + if ($this->downtime->object_type === 'host') { + $this->addHtml(new HorizontalKeyValue(t('Host'), [ + $this->downtime->host->name, + ' ', + new StateBall($this->downtime->host->state->getStateText()) + ])); + } else { + $this->addHtml(new HorizontalKeyValue(t('Service'), Html::sprintf( + t('%s on %s', '<service> on <host>'), + [ + $this->downtime->service->name, + ' ', + new StateBall($this->downtime->service->state->getStateText()) + ], + $this->downtime->host->name + ))); + } + } + + if ($this->downtime->triggered_by_id !== null || $this->downtime->parent_id !== null) { + if ($this->downtime->triggered_by_id !== null) { + $label = t('Triggered By'); + $relatedDowntime = $this->downtime->triggered_by; + } else { + $label = t('Parent'); + $relatedDowntime = $this->downtime->parent; + } + + $this->addHtml(new HorizontalKeyValue( + $label, + HtmlElement::create('span', ['class' => 'accompanying-text'], TemplateString::create( + $relatedDowntime->is_flexible + ? t('{{#link}}Flexible Downtime{{/link}} for %s') + : t('{{#link}}Fixed Downtime{{/link}} for %s'), + ['link' => new Link(null, Links::downtime($relatedDowntime), ['class' => 'subject'])], + ($relatedDowntime->object_type === 'host' + ? $this->createHostLink($relatedDowntime->host, true) + : $this->createServiceLink($relatedDowntime->service, $relatedDowntime->host, true)) + )) + )); + } + + $this->add(new HorizontalKeyValue( + t('Created'), + WebDateFormatter::formatDateTime($this->downtime->entry_time->getTimestamp()) + )); + $this->add(new HorizontalKeyValue( + t('Start time'), + $this->downtime->start_time + ? WebDateFormatter::formatDateTime($this->downtime->start_time->getTimestamp()) + : new EmptyState(t('Not started yet')) + )); + $this->add(new HorizontalKeyValue( + t('End time'), + $this->downtime->end_time + ? WebDateFormatter::formatDateTime($this->downtime->end_time->getTimestamp()) + : new EmptyState(t('Not started yet')) + )); + $this->add(new HorizontalKeyValue( + t('Scheduled Start'), + WebDateFormatter::formatDateTime($this->downtime->scheduled_start_time->getTimestamp()) + )); + $this->add(new HorizontalKeyValue( + t('Scheduled End'), + WebDateFormatter::formatDateTime($this->downtime->scheduled_end_time->getTimestamp()) + )); + $this->add(new HorizontalKeyValue( + t('Scheduled Duration'), + DateFormatter::formatDuration($this->downtime->scheduled_duration / 1000) + )); + if ($this->downtime->is_flexible) { + $this->add(new HorizontalKeyValue( + t('Flexible Duration'), + DateFormatter::formatDuration($this->downtime->flexible_duration / 1000) + )); + } + + $query = Downtime::on($this->getDb())->with([ + 'host', + 'host.state', + 'service', + 'service.host', + 'service.host.state', + 'service.state' + ]) + ->limit(3) + ->filter(Filter::equal('parent_id', $this->downtime->id)) + ->orFilter(Filter::equal('triggered_by_id', $this->downtime->id)); + $this->applyRestrictions($query); + + $children = $query->peekAhead()->execute(); + if ($children->hasResult()) { + $this->addHtml( + new HtmlElement('h2', null, Text::create(t('Children'))), + new DowntimeList($children), + (new ShowMore($children, Links::downtimes()->setQueryString( + QueryString::render(Filter::any( + Filter::equal('downtime.parent.name', $this->downtime->name), + Filter::equal('downtime.triggered_by.name', $this->downtime->name) + )) + )))->setBaseTarget('_next') + ); + } + + $this->add(Html::tag('h2', t('Progress'))); + $this->add($this->createTimeline()); + + if ( + getenv('ICINGAWEB_EXPORT_FORMAT') !== 'pdf' + && $this->isGrantedOn( + 'icingadb/command/downtime/delete', + $this->downtime->{$this->downtime->object_type} + ) + ) { + $this->add($this->createCancelDowntimeForm()); + } + } +} diff --git a/library/Icingadb/Widget/Detail/EventDetail.php b/library/Icingadb/Widget/Detail/EventDetail.php new file mode 100644 index 0000000..181c9ae --- /dev/null +++ b/library/Icingadb/Widget/Detail/EventDetail.php @@ -0,0 +1,651 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use DateTime; +use DateTimeZone; +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Widget\MarkdownText; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Model\AcknowledgementHistory; +use Icinga\Module\Icingadb\Model\CommentHistory; +use Icinga\Module\Icingadb\Model\DowntimeHistory; +use Icinga\Module\Icingadb\Model\FlappingHistory; +use Icinga\Module\Icingadb\Model\History; +use Icinga\Module\Icingadb\Model\NotificationHistory; +use Icinga\Module\Icingadb\Model\StateHistory; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Web\Widget\CopyToClipboard; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; +use Icinga\Module\Icingadb\Widget\ItemTable\UserTable; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormattedString; +use ipl\Html\HtmlElement; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Orm\ResultSet; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Str; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\StateBall; + +class EventDetail extends BaseHtmlElement +{ + use Auth; + use Database; + use HostLink; + use ServiceLink; + use TicketLinks; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'object-detail']; + + /** @var History */ + protected $event; + + public function __construct(History $event) + { + $this->event = $event; + } + + protected function assembleNotificationEvent(NotificationHistory $notification) + { + $pluginOutput = []; + + $commandName = $notification->object_type === 'host' + ? $this->event->host->checkcommand_name + : $this->event->service->checkcommand_name; + if (isset($commandName)) { + if (empty($notification->text)) { + $notificationText = new EmptyState(t('Output unavailable.')); + } else { + $notificationText = new PluginOutputContainer( + (new PluginOutput($notification->text)) + ->setCommandName($notification->object_type === 'host' + ? $this->event->host->checkcommand_name + : $this->event->service->checkcommand_name) + ); + + CopyToClipboard::attachTo($notificationText); + } + + $pluginOutput = [ + HtmlElement::create('h2', null, $notification->author ? t('Comment') : t('Plugin Output')), + HtmlElement::create('div', [ + 'id' => 'check-output-' . $commandName, + 'class' => 'collapsible', + 'data-visible-height' => 100 + ], $notificationText) + ]; + } else { + $pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.')); + } + + if ($notification->object_type === 'host') { + $objectKey = t('Host'); + $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ + HtmlElement::create('span', ['class' => 'state-change'], [ + new StateBall(HostStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM), + new StateBall(HostStates::text($notification->state), StateBall::SIZE_MEDIUM) + ]), + ' ', + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ]); + } else { + $objectKey = t('Service'); + $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ + HtmlElement::create('span', ['class' => 'state-change'], [ + new StateBall(ServiceStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM), + new StateBall(ServiceStates::text($notification->state), StateBall::SIZE_MEDIUM) + ]), + ' ', + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + ]); + } + + $eventInfo = [ + new HtmlElement('h2', null, Text::create(t('Event Info'))), + new HorizontalKeyValue( + t('Sent On'), + DateFormatter::formatDateTime($notification->send_time->getTimestamp()) + ) + ]; + + if ($notification->author) { + $eventInfo[] = (new HorizontalKeyValue(t('Sent by'), [ + new Icon('user'), + $notification->author + ])); + } + + $eventInfo[] = new HorizontalKeyValue(t('Type'), ucfirst(Str::camel($notification->type))); + $eventInfo[] = new HorizontalKeyValue(t('State'), $notification->object_type === 'host' + ? ucfirst(HostStates::text($notification->state)) + : ucfirst(ServiceStates::text($notification->state))); + $eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo); + + + $notifiedUsers = [new HtmlElement('h2', null, Text::create(t('Notified Users')))]; + + if ($notification->users_notified === 0) { + $notifiedUsers[] = new EmptyState(t('None', 'notified users: none')); + } elseif (! $this->isPermittedRoute('users')) { + $notifiedUsers[] = Text::create(sprintf(tp( + 'This notification was sent to a single user', + 'This notification was sent to %d users', + $notification->users_notified + ), $notification->users_notified)); + } elseif ($notification->users_notified > 0) { + $users = $notification->user + ->limit(5) + ->peekAhead(); + + $users = $users->execute(); + /** @var ResultSet $users */ + + $notifiedUsers[] = new UserTable($users); + $notifiedUsers[] = (new ShowMore( + $users, + Links::users()->addParams(['notification_history.id' => bin2hex($notification->id)]), + sprintf(t('Show all %d recipients'), $notification->users_notified) + ))->setBaseTarget('_next'); + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 0 => $pluginOutput, + 200 => $eventInfo, + 500 => $notifiedUsers + ], $this->createExtensions())); + } + + protected function assembleStateChangeEvent(StateHistory $stateChange) + { + $pluginOutput = []; + + $commandName = $stateChange->object_type === 'host' + ? $this->event->host->checkcommand_name + : $this->event->service->checkcommand_name; + if (isset($commandName)) { + if (empty($stateChange->output) && empty($stateChange->long_output)) { + $commandOutput = new EmptyState(t('Output unavailable.')); + } else { + $commandOutput = new PluginOutputContainer( + (new PluginOutput($stateChange->output . "\n" . $stateChange->long_output)) + ->setCommandName($commandName) + ); + + CopyToClipboard::attachTo($commandOutput); + } + + $pluginOutput = [ + new HtmlElement('h2', null, Text::create(t('Plugin Output'))), + HtmlElement::create('div', [ + 'id' => 'check-output-' . $commandName, + 'class' => 'collapsible', + 'data-visible-height' => 100 + ], $commandOutput) + ]; + } else { + $pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.')); + } + + if ($stateChange->object_type === 'host') { + $objectKey = t('Host'); + $objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state; + $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ + HtmlElement::create('span', ['class' => 'state-change'], [ + new StateBall(HostStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM), + new StateBall(HostStates::text($objectState), StateBall::SIZE_MEDIUM) + ]), + ' ', + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ]); + } else { + $objectKey = t('Service'); + $objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state; + $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ + HtmlElement::create('span', ['class' => 'state-change'], [ + new StateBall(ServiceStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM), + new StateBall(ServiceStates::text($objectState), StateBall::SIZE_MEDIUM) + ]), + ' ', + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + ]); + } + + $eventInfo = [ + new HtmlElement('h2', null, Text::create(t('Event Info'))), + new HorizontalKeyValue( + t('Occurred On'), + DateFormatter::formatDateTime($stateChange->event_time->getTimestamp()) + ), + new HorizontalKeyValue(t('Scheduling Source'), $stateChange->scheduling_source), + new HorizontalKeyValue(t('Check Source'), $stateChange->check_source) + ]; + + if ($stateChange->state_type === 'soft') { + $eventInfo[] = new HorizontalKeyValue(t('Check Attempt'), sprintf( + t('%d of %d'), + $stateChange->check_attempt, + $stateChange->max_check_attempts + )); + } + + $eventInfo[] = new HorizontalKeyValue( + t('State'), + $stateChange->object_type === 'host' + ? ucfirst(HostStates::text($objectState)) + : ucfirst(ServiceStates::text($objectState)) + ); + + $eventInfo[] = new HorizontalKeyValue( + t('State Type'), + $stateChange->state_type === 'hard' ? t('Hard', 'state') : t('Soft', 'state') + ); + + $eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo); + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 0 => $pluginOutput, + 200 => $eventInfo + ], $this->createExtensions())); + } + + protected function assembleDowntimeEvent(DowntimeHistory $downtime) + { + $commentInfo = [ + new HtmlElement('h2', null, Text::create(t('Comment'))), + new MarkdownText($this->createTicketLinks($downtime->comment)) + ]; + + $eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))]; + + if ($downtime->triggered_by_id !== null || $downtime->parent_id !== null) { + if ($downtime->triggered_by_id !== null) { + $label = t('Triggered By'); + $relatedDowntime = $downtime->triggered_by; + } else { + $label = t('Parent'); + $relatedDowntime = $downtime->parent; + } + + $query = History::on($this->getDb()) + ->columns('id') + ->filter(Filter::equal('event_type', 'downtime_start')) + ->filter(Filter::equal('history.downtime_history_id', $relatedDowntime->downtime_id)); + $this->applyRestrictions($query); + if (($relatedEvent = $query->first()) !== null) { + /** @var History $relatedEvent */ + $eventInfo[] = new HorizontalKeyValue( + $label, + HtmlElement::create('span', ['class' => 'accompanying-text'], TemplateString::create( + $relatedDowntime->is_flexible + ? t('{{#link}}Flexible Downtime{{/link}} for %s') + : t('{{#link}}Fixed Downtime{{/link}} for %s'), + ['link' => new Link(null, Links::event($relatedEvent), ['class' => 'subject'])], + ($relatedDowntime->object_type === 'host' + ? $this->createHostLink($relatedDowntime->host, true) + : $this->createServiceLink($relatedDowntime->service, $relatedDowntime->host, true)) + ->addAttributes(['class' => 'subject']) + )) + ); + } + } + + $eventInfo[] = $downtime->object_type === 'host' + ? new HorizontalKeyValue(t('Host'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + )) + : new HorizontalKeyValue(t('Service'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + )); + $eventInfo[] = new HorizontalKeyValue( + t('Entered On'), + DateFormatter::formatDateTime($downtime->entry_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $downtime->author]); + // TODO: The following should be presented in a specific widget (maybe just like the downtime card) + $eventInfo[] = new HorizontalKeyValue( + t('Triggered On'), + DateFormatter::formatDateTime($downtime->trigger_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue( + t('Scheduled Start'), + DateFormatter::formatDateTime($downtime->scheduled_start_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue( + t('Actual Start'), + DateFormatter::formatDateTime($downtime->start_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue( + t('Scheduled End'), + DateFormatter::formatDateTime($downtime->scheduled_end_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue( + t('Actual End'), + DateFormatter::formatDateTime($downtime->end_time->getTimestamp()) + ); + + if ($downtime->is_flexible) { + $eventInfo[] = new HorizontalKeyValue(t('Flexible'), t('Yes')); + $eventInfo[] = new HorizontalKeyValue( + t('Duration'), + DateFormatter::formatDuration($downtime->flexible_duration / 1000) + ); + } + + $cancelInfo = []; + if ($downtime->has_been_cancelled) { + $cancelInfo = [ + new HtmlElement('h2', null, Text::create(t('This downtime has been cancelled'))), + new HorizontalKeyValue( + t('Cancelled On'), + DateFormatter::formatDateTime($downtime->cancel_time->getTimestamp()) + ), + new HorizontalKeyValue(t('Cancelled by'), [new Icon('user'), $downtime->cancelled_by]) + ]; + } + + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 200 => $commentInfo, + 201 => $eventInfo, + 600 => $cancelInfo + ], $this->createExtensions())); + } + + protected function assembleCommentEvent(CommentHistory $comment) + { + $commentInfo = [ + new HtmlElement('h2', null, Text::create(t('Comment'))), + new MarkdownText($this->createTicketLinks($comment->comment)) + ]; + + $eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))]; + $eventInfo[] = $comment->object_type === 'host' + ? new HorizontalKeyValue(t('Host'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + )) + : new HorizontalKeyValue(t('Service'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + )); + $eventInfo[] = new HorizontalKeyValue( + t('Entered On'), + DateFormatter::formatDateTime($comment->entry_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $comment->author]); + $eventInfo[] = new HorizontalKeyValue( + t('Expires On'), + $comment->expire_time + ? DateFormatter::formatDateTime($comment->expire_time->getTimestamp()) + : new EmptyState(t('Never')) + ); + + $tiedToAckInfo = []; + if ($comment->entry_type === 'ack') { + $tiedToAckInfo = [ + new HtmlElement('h2', null, Text::create(t('This comment is tied to an acknowledgement'))), + new HorizontalKeyValue(t('Sticky'), $comment->is_sticky ? t('Yes') : t('No')), + new HorizontalKeyValue(t('Persistent'), $comment->is_persistent ? t('Yes') : t('No')) + ]; + } + + $removedInfo = []; + if ($comment->has_been_removed) { + $removedInfo[] = new HtmlElement('h2', null, Text::create(t('This comment has been removed'))); + if ($comment->removed_by) { + $removedInfo[] = new HorizontalKeyValue( + t('Removed On'), + DateFormatter::formatDateTime($comment->remove_time->getTimestamp()) + ); + $removedInfo[] = new HorizontalKeyValue( + t('Removed by'), + [new Icon('user'), $comment->removed_by] + ); + } else { + $removedInfo[] = new HorizontalKeyValue( + t('Expired On'), + DateFormatter::formatDateTime($comment->remove_time->getTimestamp()) + ); + } + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 200 => $commentInfo, + 201 => $eventInfo, + 500 => $tiedToAckInfo, + 600 => $removedInfo + ], $this->createExtensions())); + } + + protected function assembleFlappingEvent(FlappingHistory $flapping) + { + $eventInfo = [ + new HtmlElement('h2', null, Text::create(t('Event Info'))), + $flapping->object_type === 'host' + ? new HorizontalKeyValue(t('Host'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + )) + : new HorizontalKeyValue(t('Service'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + )), + new HorizontalKeyValue( + t('Started on'), + DateFormatter::formatDateTime($flapping->start_time->getTimestamp()) + ) + ]; + if ($this->event->event_type === 'flapping_start') { + $eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf( + t('State change rate of %.2f%% exceeded the threshold (%.2f%%)'), + $flapping->percent_state_change_start, + $flapping->flapping_threshold_high + )); + } else { + $eventInfo[] = new HorizontalKeyValue( + t('Ended on'), + DateFormatter::formatDateTime($flapping->end_time->getTimestamp()) + ); + $eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf( + t('State change rate of %.2f%% undercut the threshold (%.2f%%)'), + $flapping->percent_state_change_end, + $flapping->flapping_threshold_low + )); + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 200 => $eventInfo + ], $this->createExtensions())); + } + + protected function assembleAcknowledgeEvent(AcknowledgementHistory $acknowledgement) + { + $commentInfo = []; + if ($acknowledgement->comment) { + $commentInfo = [ + new HtmlElement('h2', null, Text::create(t('Comment'))), + new MarkdownText($this->createTicketLinks($acknowledgement->comment)) + ]; + } elseif (! isset($acknowledgement->author)) { + $commentInfo[] = new EmptyState(t('This acknowledgement was set before Icinga DB history recording')); + } + + $eventInfo = [ + new HtmlElement('h2', null, Text::create(t('Event Info'))), + new HorizontalKeyValue( + t('Set on'), + DateFormatter::formatDateTime($acknowledgement->set_time->getTimestamp()) + ), + new HorizontalKeyValue(t('Author'), $acknowledgement->author + ? [new Icon('user'), $acknowledgement->author] + : new EmptyState(t('n. a.'))), + $acknowledgement->object_type === 'host' + ? new HorizontalKeyValue(t('Host'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + )) + : new HorizontalKeyValue(t('Service'), HtmlElement::create( + 'span', + ['class' => 'accompanying-text'], + FormattedString::create( + t('%s on %s', '<service> on <host>'), + HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), + HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) + ) + )) + ]; + + if ($this->event->event_type === 'ack_set') { + $eventInfo[] = new HorizontalKeyValue( + t('Expires On'), + $acknowledgement->expire_time + ? DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp()) + : new EmptyState(t('Never')) + ); + $eventInfo[] = new HorizontalKeyValue(t('Sticky'), isset($acknowledgement->is_sticky) + ? ($acknowledgement->is_sticky ? t('Yes') : t('No')) + : new EmptyState(t('n. a.'))); + $eventInfo[] = new HorizontalKeyValue(t('Persistent'), isset($acknowledgement->is_persistent) + ? ($acknowledgement->is_persistent ? t('Yes') : t('No')) + : new EmptyState(t('n. a.'))); + } else { + $eventInfo[] = new HorizontalKeyValue( + t('Cleared on'), + DateFormatter::formatDateTime( + $acknowledgement->clear_time + ? $acknowledgement->clear_time->getTimestamp() + : $this->event->event_time->getTimestamp() + ) + ); + if ($acknowledgement->cleared_by) { + $eventInfo[] = new HorizontalKeyValue( + t('Cleared by'), + [new Icon('user', $acknowledgement->cleared_by)] + ); + } else { + $expired = false; + if ($acknowledgement->expire_time) { + $now = (new DateTime())->setTimezone(new DateTimeZone('UTC')); + $expiresOn = clone $now; + $expiresOn->setTimestamp($acknowledgement->expire_time->getTimestamp()); + if ($now <= $expiresOn) { + $expired = true; + $eventInfo[] = new HorizontalKeyValue(t('Removal Reason'), t( + 'The acknowledgement expired on %s', + DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp()) + )); + } + } + + if (! $expired) { + if ($acknowledgement->is_sticky) { + $eventInfo[] = new HorizontalKeyValue( + t('Reason'), + $acknowledgement->object_type === 'host' + ? t('Host recovered') + : t('Service recovered') + ); + } else { + $eventInfo[] = new HorizontalKeyValue( + t('Reason'), + $acknowledgement->object_type === 'host' + ? t('Host recovered') // Hosts have no other state between UP and DOWN + : t('Service changed its state') + ); + } + } + } + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 200 => $commentInfo, + 201 => $eventInfo + ], $this->createExtensions())); + } + + protected function createExtensions(): array + { + return ObjectDetailExtensionHook::loadExtensions($this->event); + } + + protected function assemble() + { + switch ($this->event->event_type) { + case 'notification': + $this->assembleNotificationEvent($this->event->notification); + + break; + case 'state_change': + $this->assembleStateChangeEvent($this->event->state); + + break; + case 'downtime_start': + case 'downtime_end': + $this->assembleDowntimeEvent($this->event->downtime); + + break; + case 'comment_add': + case 'comment_remove': + $this->assembleCommentEvent($this->event->comment); + + break; + case 'flapping_start': + case 'flapping_end': + $this->assembleFlappingEvent($this->event->flapping); + + break; + case 'ack_set': + case 'ack_clear': + $this->assembleAcknowledgeEvent($this->event->acknowledgement); + + break; + } + } +} diff --git a/library/Icingadb/Widget/Detail/HostDetail.php b/library/Icingadb/Widget/Detail/HostDetail.php new file mode 100644 index 0000000..8b80480 --- /dev/null +++ b/library/Icingadb/Widget/Detail/HostDetail.php @@ -0,0 +1,58 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use ipl\Html\Html; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\EmptyState; + +class HostDetail extends ObjectDetail +{ + protected $serviceSummary; + + public function __construct(Host $object, ServicestateSummary $serviceSummary) + { + parent::__construct($object); + + $this->serviceSummary = $serviceSummary; + } + + protected function createServiceStatistics(): array + { + if ($this->serviceSummary->services_total > 0) { + $services = new ServiceStatistics($this->serviceSummary); + $services->setBaseFilter(Filter::equal('host.name', $this->object->name)); + } else { + $services = new EmptyState(t('This host has no services')); + } + + return [Html::tag('h2', t('Services')), $services]; + } + + protected function assemble() + { + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + $this->add($this->createPrintHeader()); + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 0 => $this->createPluginOutput(), + 190 => $this->createServiceStatistics(), + 300 => $this->createActions(), + 301 => $this->createNotes(), + 400 => $this->createComments(), + 401 => $this->createDowntimes(), + 500 => $this->createGroups(), + 501 => $this->createNotifications(), + 600 => $this->createCheckStatistics(), + 601 => $this->createPerformanceData(), + 700 => $this->createCustomVars(), + 701 => $this->createFeatureToggles() + ], $this->createExtensions())); + } +} diff --git a/library/Icingadb/Widget/Detail/HostInspectionDetail.php b/library/Icingadb/Widget/Detail/HostInspectionDetail.php new file mode 100644 index 0000000..93b35b8 --- /dev/null +++ b/library/Icingadb/Widget/Detail/HostInspectionDetail.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\ObjectInspectionDetail; + +class HostInspectionDetail extends ObjectInspectionDetail +{ + protected function assemble() + { + $this->add([ + $this->createSourceLocation(), + $this->createLastCheckResult(), + $this->createAttributes(), + $this->createCustomVariables(), + $this->createRedisInfo() + ]); + } +} diff --git a/library/Icingadb/Widget/Detail/HostMetaInfo.php b/library/Icingadb/Widget/Detail/HostMetaInfo.php new file mode 100644 index 0000000..78209aa --- /dev/null +++ b/library/Icingadb/Widget/Detail/HostMetaInfo.php @@ -0,0 +1,77 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; + +class HostMetaInfo extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'object-meta-info']; + + /** @var Host */ + protected $host; + + public function __construct(Host $host) + { + $this->host = $host; + } + + protected function assemble() + { + $this->addHtml( + new VerticalKeyValue('host.name', $this->host->name), + new HtmlElement( + 'div', + null, + new HorizontalKeyValue( + 'host.address', + $this->host->address ?: new EmptyState(t('None', 'address')) + ), + new HorizontalKeyValue( + 'host.address6', + $this->host->address6 ?: new EmptyState(t('None', 'address')) + ) + ), + new VerticalKeyValue( + 'last_state_change', + $this->host->state->last_state_change !== null + ? DateFormatter::formatDateTime($this->host->state->last_state_change->getTimestamp()) + : (new EmptyState(t('n. a.')))->setTag('span') + ) + ); + + $collapsible = new HtmlElement('div', Attributes::create([ + 'class' => 'collapsible', + 'id' => 'object-meta-info', + 'data-toggle-element' => '.object-meta-info-control', + 'data-visible-height' => 0 + ])); + + $renderHelper = new HtmlDocument(); + $renderHelper->addHtml( + $this, + new HtmlElement( + 'button', + Attributes::create(['class' => 'object-meta-info-control']), + new Icon('angle-double-up', ['class' => 'collapse-icon']), + new Icon('angle-double-down', ['class' => 'expand-icon']) + ) + ); + + $this->addWrapper($collapsible); + $this->addWrapper($renderHelper); + } +} diff --git a/library/Icingadb/Widget/Detail/HostStatistics.php b/library/Icingadb/Widget/Detail/HostStatistics.php new file mode 100644 index 0000000..bcfc3f8 --- /dev/null +++ b/library/Icingadb/Widget/Detail/HostStatistics.php @@ -0,0 +1,61 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Chart\Donut; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Widget\HostStateBadges; +use ipl\Html\ValidHtml; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\HtmlString; +use ipl\Web\Filter\QueryString; +use ipl\Web\Widget\Link; + +class HostStatistics extends ObjectStatistics +{ + protected $summary; + + public function __construct($summary) + { + $this->summary = $summary; + } + + protected function createDonut(): ValidHtml + { + $donut = (new Donut()) + ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']); + + return HtmlString::create($donut->render()); + } + + protected function createTotal(): ValidHtml + { + $url = Links::hosts(); + if ($this->hasBaseFilter()) { + $url->setFilter($this->getBaseFilter()); + } + + return new Link( + (new VerticalKeyValue( + tp('Host', 'Hosts', $this->summary->hosts_total), + $this->shortenAmount($this->summary->hosts_total) + ))->setAttribute('title', $this->summary->hosts_total), + $url + ); + } + + protected function createBadges(): ValidHtml + { + $badges = new HostStateBadges($this->summary); + if ($this->hasBaseFilter()) { + $badges->setBaseFilter($this->getBaseFilter()); + } + + return $badges; + } +} diff --git a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php new file mode 100644 index 0000000..f398d80 --- /dev/null +++ b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php @@ -0,0 +1,194 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; +use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Stdlib\BaseFilter; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; +use ipl\Web\Widget\Icon; + +class MultiselectQuickActions extends BaseHtmlElement +{ + use BaseFilter; + use Auth; + + protected $summary; + + protected $type; + + protected $tag = 'ul'; + + protected $defaultAttributes = ['class' => 'quick-actions']; + + public function __construct($type, $summary) + { + $this->summary = $summary; + $this->type = $type; + } + + protected function assemble() + { + $unacknowledged = "{$this->type}s_problems_unacknowledged"; + $acks = "{$this->type}s_acknowledged"; + $activeChecks = "{$this->type}s_active_checks_enabled"; + + if ( + $this->summary->$unacknowledged > $this->summary->$acks + && $this->isGrantedOnType( + 'icingadb/command/acknowledge-problem', + $this->type, + $this->getBaseFilter(), + false + ) + ) { + $this->assembleAction( + 'acknowledge', + t('Acknowledge'), + 'check-circle', + t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') + ); + } + + if ( + $this->summary->$acks > 0 + && $this->isGrantedOnType( + 'icingadb/command/remove-acknowledgement', + $this->type, + $this->getBaseFilter(), + false + ) + ) { + $removeAckForm = (new RemoveAcknowledgementForm()) + ->setAction($this->getLink('removeAcknowledgement')) + ->setObjects(array_fill(0, $this->summary->$acks, null)); + + $this->add(Html::tag('li', $removeAckForm)); + } + + if ( + $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) + || ( + $this->summary->$activeChecks > 0 + && $this->isGrantedOnType( + 'icingadb/command/schedule-check/active-only', + $this->type, + $this->getBaseFilter(), + false + ) + ) + ) { + $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); + } + + if ($this->isGrantedOnType('icingadb/command/comment/add', $this->type, $this->getBaseFilter(), false)) { + $this->assembleAction( + 'addComment', + t('Comment'), + 'comment', + t('Add a new comment') + ); + } + + if ( + $this->isGrantedOnType( + 'icingadb/command/send-custom-notification', + $this->type, + $this->getBaseFilter(), + false + ) + ) { + $this->assembleAction( + 'sendCustomNotification', + t('Notification'), + 'bell', + t('Send a custom notification') + ); + } + + if ( + $this->isGrantedOnType( + 'icingadb/command/downtime/schedule', + $this->type, + $this->getBaseFilter(), + false + ) + ) { + $this->assembleAction( + 'scheduleDowntime', + t('Downtime'), + 'plug', + t('Schedule a downtime to suppress all problem notifications within a specific period of time') + ); + } + + if ( + $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) + || ( + $this->summary->$activeChecks > 0 + && $this->isGrantedOnType( + 'icingadb/command/schedule-check/active-only', + $this->type, + $this->getBaseFilter(), + false + ) + ) + ) { + $this->assembleAction( + 'scheduleCheck', + t('Reschedule'), + 'calendar', + t('Schedule the next active check at a different time than the current one') + ); + } + + if ( + $this->isGrantedOnType( + 'icingadb/command/process-check-result', + $this->type, + $this->getBaseFilter(), + false + ) + ) { + $this->assembleAction( + 'processCheckresult', + t('Process check result'), + 'edit', + t('Submit passive check result') + ); + } + } + + protected function assembleAction(string $action, string $label, string $icon, string $title) + { + $link = Html::tag( + 'a', + [ + 'href' => $this->getLink($action), + 'class' => 'action-link', + 'title' => $title, + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ], + [ + new Icon($icon), + $label + ] + ); + + $this->add(Html::tag('li', $link)); + } + + protected function getLink(string $action): string + { + return Url::fromPath("icingadb/{$this->type}s/$action") + ->setFilter($this->getBaseFilter()) + ->getAbsoluteUrl(); + } +} diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php new file mode 100644 index 0000000..a688173 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -0,0 +1,596 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Exception; +use Icinga\Application\ClassLoader; +use Icinga\Application\Hook; +use Icinga\Application\Hook\GrapherHook; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Application\Web; +use Icinga\Date\DateFormatter; +use Icinga\Exception\IcingaException; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\HostLinks; +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\Macros; +use Icinga\Module\Icingadb\Compat\CompatHost; +use Icinga\Module\Icingadb\Compat\CompatService; +use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Web\Navigation\Action; +use Icinga\Module\Icingadb\Widget\MarkdownText; +use Icinga\Module\Icingadb\Common\ServiceLinks; +use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; +use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\User; +use Icinga\Module\Icingadb\Model\Usergroup; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList; +use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Web\Widget\CopyToClipboard; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; +use Icinga\Module\Icingadb\Widget\ItemList\CommentList; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use Icinga\Module\Icingadb\Widget\TagList; +use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; +use Icinga\Web\Navigation\Navigation; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Orm\ResultSet; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; + +class ObjectDetail extends BaseHtmlElement +{ + use Auth; + use Database; + use Macros; + + protected $object; + + protected $compatObject; + + protected $objectType; + + protected $defaultAttributes = [ + // Class host-detail is kept as the grafana module's iframe.js depends on it + 'class' => ['object-detail', 'host-detail'], + 'data-pdfexport-page-breaks-at' => 'h2' + ]; + + protected $tag = 'div'; + + public function __construct($object) + { + $this->object = $object; + $this->objectType = $object instanceof Host ? 'host' : 'service'; + } + + protected function compatObject() + { + if ($this->compatObject === null) { + $this->compatObject = CompatHost::fromModel($this->object); + } + + return $this->compatObject; + } + + protected function createPrintHeader() + { + $info = [new HorizontalKeyValue(t('Name'), $this->object->name)]; + + if ($this->objectType === 'host') { + $info[] = new HorizontalKeyValue( + t('IPv4 Address'), + $this->object->address ?: new EmptyState(t('None', 'address')) + ); + $info[] = new HorizontalKeyValue( + t('IPv6 Address'), + $this->object->address6 ?: new EmptyState(t('None', 'address')) + ); + } + + $info[] = new HorizontalKeyValue(t('State'), [ + $this->object->state->getStateTextTranslated(), + ' ', + new StateBall($this->object->state->getStateText()) + ]); + + $info[] = new HorizontalKeyValue( + t('Last State Change'), + DateFormatter::formatDateTime($this->object->state->last_state_change->getTimestamp()) + ); + + return [ + new HtmlElement('h2', null, Text::create( + $this->objectType === 'host' ? t('Host') : t('Service') + )), + $info + ]; + } + + protected function createActions() + { + $this->fetchCustomVars(); + + $navigation = new Navigation(); + $navigation->load('icingadb-' . $this->objectType . '-action'); + /** @var Action $item */ + foreach ($navigation as $item) { + $item->setObject($this->object); + } + + $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); + $obj = $monitoringInstalled ? $this->compatObject() : $this->object; + foreach ($this->object->action_url->first()->action_url ?? [] as $url) { + $url = $this->expandMacros($url, $obj); + $navigation->addItem( + Html::wantHtml([ + // Add warning to links that open in new tabs, as recommended by WCAG20 G201 + new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), + $url + ])->render(), + [ + 'target' => '_blank', + 'url' => $url, + 'renderer' => [ + 'NavigationItemRenderer', + 'escape_label' => false + ] + ] + ); + } + + $moduleActions = ObjectActionsHook::loadActions($this->object); + + $nativeExtensionProviders = []; + foreach ($moduleActions->getContent() as $item) { + if ($item->getAttributes()->has('data-icinga-module')) { + $nativeExtensionProviders[$item->getAttributes()->get('data-icinga-module')->getValue()] = true; + } + } + + if (Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { + foreach (Hook::all('Monitoring\\' . ucfirst($this->objectType) . 'Actions') as $hook) { + $moduleName = ClassLoader::extractModuleName(get_class($hook)); + if (! isset($nativeExtensionProviders[$moduleName])) { + try { + $navigation->merge($hook->getNavigation($this->compatObject())); + } catch (Exception $e) { + Logger::error("Failed to load legacy action hook: %s\n%s", $e, $e->getTraceAsString()); + $navigation->addItem($moduleName, ['label' => IcingaException::describe($e), 'url' => '#']); + } + } + } + } + + if ($moduleActions->isEmpty() && ($navigation->isEmpty() || ! $navigation->hasRenderableItems())) { + return null; + } + + return [ + Html::tag('h2', t('Actions')), + new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()), + $moduleActions->isEmpty() ? null : $moduleActions + ]; + } + + protected function createCheckStatistics(): array + { + return [ + Html::tag('h2', t('Check Statistics')), + new CheckStatistics($this->object) + ]; + } + + protected function createComments(): array + { + if ($this->objectType === 'host') { + $link = HostLinks::comments($this->object); + $relations = ['host', 'host.state']; + } else { + $link = ServiceLinks::comments($this->object, $this->object->host); + $relations = ['service', 'service.state', 'service.host', 'service.host.state']; + } + + $comments = $this->object->comment + ->with($relations) + ->limit(3) + ->peekAhead(); + // TODO: This should be automatically done by the model/resolver and added as ON condition + $comments->filter(Filter::equal('object_type', $this->objectType)); + + $comments = $comments->execute(); + /** @var ResultSet $comments */ + + $content = [Html::tag('h2', t('Comments'))]; + + if ($comments->hasResult()) { + $content[] = (new CommentList($comments))->setObjectLinkDisabled()->setTicketLinkEnabled(); + $content[] = (new ShowMore($comments, $link))->setBaseTarget('_next'); + } else { + $content[] = new EmptyState(t('No comments created.')); + } + + return $content; + } + + protected function createCustomVars(): array + { + $content = [Html::tag('h2', t('Custom Variables'))]; + + $this->fetchCustomVars(); + $vars = (new CustomvarFlat())->unFlattenVars($this->object->customvar_flat); + if (! empty($vars)) { + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => $this->objectType . '-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->object)); + } else { + $content[] = new EmptyState(t('No custom variables configured.')); + } + + return $content; + } + + protected function createDowntimes(): array + { + if ($this->objectType === 'host') { + $link = HostLinks::downtimes($this->object); + $relations = ['host', 'host.state']; + } else { + $link = ServiceLinks::downtimes($this->object, $this->object->host); + $relations = ['service', 'service.state', 'service.host', 'service.host.state']; + } + + $downtimes = $this->object->downtime + ->with($relations) + ->limit(3) + ->peekAhead(); + // TODO: This should be automatically done by the model/resolver and added as ON condition + $downtimes->filter(Filter::equal('object_type', $this->objectType)); + + $downtimes = $downtimes->execute(); + /** @var ResultSet $downtimes */ + + $content = [Html::tag('h2', t('Downtimes'))]; + + if ($downtimes->hasResult()) { + $content[] = (new DowntimeList($downtimes))->setObjectLinkDisabled()->setTicketLinkEnabled(); + $content[] = (new ShowMore($downtimes, $link))->setBaseTarget('_next'); + } else { + $content[] = new EmptyState(t('No downtimes scheduled.')); + } + + return $content; + } + + protected function createGroups(): array + { + $groups = [Html::tag('h2', t('Groups'))]; + + if ($this->objectType === 'host') { + $hostgroups = []; + if ($this->isPermittedRoute('hostgroups')) { + $hostgroups = $this->object->hostgroup; + $this->applyRestrictions($hostgroups); + } + + $hostgroupList = new TagList(); + foreach ($hostgroups as $hostgroup) { + $hostgroupList->addLink($hostgroup->display_name, Links::hostgroup($hostgroup)); + } + + $groups[] = $hostgroupList->hasContent() + ? $hostgroupList + : new EmptyState(t('Not a member of any host group.')); + } else { + $servicegroups = []; + if ($this->isPermittedRoute('servicegroups')) { + $servicegroups = $this->object->servicegroup; + $this->applyRestrictions($servicegroups); + } + + $servicegroupList = new TagList(); + foreach ($servicegroups as $servicegroup) { + $servicegroupList->addLink($servicegroup->display_name, Links::servicegroup($servicegroup)); + } + + $groups[] = $servicegroupList->hasContent() + ? $servicegroupList + : new EmptyState(t('Not a member of any service group.')); + } + + return $groups; + } + + protected function createNotes() + { + $navigation = new Navigation(); + $notes = trim($this->object->notes); + + $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); + $obj = $monitoringInstalled ? $this->compatObject() : $this->object; + foreach ($this->object->notes_url->first()->notes_url ?? [] as $url) { + $url = $this->expandMacros($url, $obj); + $navigation->addItem( + Html::wantHtml([ + // Add warning to links that open in new tabs, as recommended by WCAG20 G201 + new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), + $url + ])->render(), + [ + 'target' => '_blank', + 'url' => $url, + 'renderer' => [ + 'NavigationItemRenderer', + 'escape_label' => false + ] + ] + ); + } + + $content = []; + + if (! $navigation->isEmpty() && $navigation->hasRenderableItems()) { + $content[] = new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()); + } + + if ($notes !== '') { + $content[] = (new MarkdownText($notes)) + ->addAttributes([ + 'class' => 'collapsible', + 'data-visible-height' => 200, + 'id' => $this->objectType . '-notes' + ]); + } + + if (empty($content)) { + return null; + } + + array_unshift($content, Html::tag('h2', t('Notes'))); + + return $content; + } + + protected function createNotifications(): array + { + list($users, $usergroups) = $this->getUsersAndUsergroups(); + + $userList = new TagList(); + $usergroupList = new TagList(); + + foreach ($users as $user) { + $userList->addLink([new Icon(Icons::USER), $user->display_name], Links::user($user)); + } + + foreach ($usergroups as $usergroup) { + $usergroupList->addLink( + [new Icon(Icons::USERGROUP), $usergroup->display_name], + Links::usergroup($usergroup) + ); + } + + return [ + Html::tag('h2', t('Notifications')), + new HorizontalKeyValue( + t('Users'), + $userList->hasContent() ? $userList : new EmptyState(t('No users configured.')) + ), + new HorizontalKeyValue( + t('User Groups'), + $usergroupList->hasContent() + ? $usergroupList + : new EmptyState(t('No user groups configured.')) + ) + ]; + } + + protected function createPerformanceData(): array + { + $content[] = Html::tag('h2', t('Performance Data')); + + if (empty($this->object->state->performance_data)) { + $content[] = new EmptyState(t('No performance data available.')); + } else { + $content[] = new HtmlElement( + 'div', + Attributes::create(['id' => 'check-perfdata-' . $this->object->checkcommand_name]), + new PerfDataTable($this->object->state->normalized_performance_data) + ); + } + + return $content; + } + + protected function createPluginOutput(): array + { + if (empty($this->object->state->output) && empty($this->object->state->long_output)) { + $pluginOutput = new EmptyState(t('Output unavailable.')); + } else { + $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->object)); + CopyToClipboard::attachTo($pluginOutput); + } + + return [ + Html::tag('h2', t('Plugin Output')), + Html::tag( + 'div', + [ + 'id' => 'check-output-' . $this->object->checkcommand_name, + 'class' => 'collapsible', + 'data-visible-height' => 100 + ], + $pluginOutput + ) + ]; + } + + protected function createExtensions(): array + { + $extensions = ObjectDetailExtensionHook::loadExtensions($this->object); + + $nativeExtensionProviders = []; + foreach ($extensions as $extension) { + if ($extension instanceof BaseHtmlElement && $extension->getAttributes()->has('data-icinga-module')) { + $nativeExtensionProviders[$extension->getAttributes()->get('data-icinga-module')->getValue()] = true; + } + } + + if (! Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { + return $extensions; + } + + foreach (Hook::all('Grapher') as $grapher) { + /** @var GrapherHook $grapher */ + $moduleName = ClassLoader::extractModuleName(get_class($grapher)); + + if (isset($nativeExtensionProviders[$moduleName])) { + continue; + } + + try { + $graph = HtmlString::create($grapher->getPreviewHtml($this->compatObject())); + } catch (Exception $e) { + Logger::error("Failed to load legacy grapher: %s\n%s", $e, $e->getTraceAsString()); + $graph = Text::create(IcingaException::describe($e)); + } + + $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::GRAPH_SECTION]; + while (isset($extensions[$location])) { + $location++; + } + + $extensions[$location] = $graph; + } + + foreach (Hook::all('Monitoring\DetailviewExtension') as $extension) { + /** @var DetailviewExtensionHook $extension */ + $moduleName = $extension->getModule()->getName(); + + if (isset($nativeExtensionProviders[$moduleName])) { + continue; + } + + try { + /** @var Web $app */ + $app = Icinga::app(); + $renderedExtension = $extension + ->setView($app->getViewRenderer()->view) + ->getHtmlForObject($this->compatObject()); + + $extensionHtml = new HtmlElement( + 'div', + Attributes::create([ + 'class' => 'icinga-module module-' . $moduleName, + 'data-icinga-module' => $moduleName + ]), + HtmlString::create($renderedExtension) + ); + } catch (Exception $e) { + Logger::error("Failed to load legacy detail extension: %s\n%s", $e, $e->getTraceAsString()); + $extensionHtml = Text::create(IcingaException::describe($e)); + } + + $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::DETAIL_SECTION]; + while (isset($extensions[$location])) { + $location++; + } + + $extensions[$location] = $extensionHtml; + } + + return $extensions; + } + + protected function createFeatureToggles(): array + { + $form = new ToggleObjectFeaturesForm($this->object); + + if ($this->objectType === 'host') { + $form->setAction(HostLinks::toggleFeatures($this->object)->getAbsoluteUrl()); + } else { + $form->setAction(ServiceLinks::toggleFeatures($this->object, $this->object->host)->getAbsoluteUrl()); + } + + return [ + Html::tag('h2', t('Feature Commands')), + $form + ]; + } + + protected function getUsersAndUsergroups(): array + { + $users = []; + $usergroups = []; + $groupBy = false; + + if ($this->objectType === 'host') { + $objectFilter = Filter::all( + Filter::equal('notification.host_id', $this->object->id), + Filter::unlike('notification.service_id', '*') + ); + $objectFilter->metaData()->set('forceOptimization', false); + $groupBy = true; + } else { + $objectFilter = Filter::equal( + 'notification.service_id', + $this->object->id + ); + } + + $userQuery = null; + if ($this->isPermittedRoute('users')) { + $userQuery = User::on($this->getDb()); + $userQuery->filter($objectFilter); + $this->applyRestrictions($userQuery); + if ($groupBy) { + $userQuery->getSelectBase()->groupBy(['user.id']); + } + + foreach ($userQuery as $user) { + $users[$user->name] = $user; + } + } + + if ($this->isPermittedRoute('usergroups')) { + $usergroupQuery = Usergroup::on($this->getDb()); + $usergroupQuery->filter($objectFilter); + $this->applyRestrictions($usergroupQuery); + if ($groupBy && $userQuery !== null) { + $userQuery->getSelectBase()->groupBy(['usergroup.id']); + } + + foreach ($usergroupQuery as $usergroup) { + $usergroups[$usergroup->name] = $usergroup; + } + } + + return [$users, $usergroups]; + } + + protected function fetchCustomVars() + { + $customvarFlat = $this->object->customvar_flat; + if (! $customvarFlat instanceof ResultSet) { + $this->applyRestrictions($customvarFlat); + $customvarFlat->withColumns(['customvar.name', 'customvar.value']); + $this->object->customvar_flat = $customvarFlat->execute(); + } + } +} diff --git a/library/Icingadb/Widget/Detail/ObjectStatistics.php b/library/Icingadb/Widget/Detail/ObjectStatistics.php new file mode 100644 index 0000000..477bf5f --- /dev/null +++ b/library/Icingadb/Widget/Detail/ObjectStatistics.php @@ -0,0 +1,59 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\ValidHtml; +use ipl\Stdlib\BaseFilter; + +abstract class ObjectStatistics extends BaseHtmlElement +{ + use BaseFilter; + + protected $tag = 'ul'; + + protected $defaultAttributes = ['class' => 'object-statistics']; + + abstract protected function createDonut(): ValidHtml; + + abstract protected function createTotal(): ValidHtml; + + abstract protected function createBadges(): ValidHtml; + + /** + * Shorten the given amount to 4 characters max + * + * @param int $amount + * + * @return string + */ + protected function shortenAmount(int $amount): string + { + if ($amount < 10000) { + return (string) $amount; + } + + if ($amount < 999500) { + return sprintf('%dk', round($amount / 1000.0)); + } + + if ($amount < 9959000) { + return sprintf('%.1fM', $amount / 1000000.0); + } + + // I think we can rule out amounts over 1 Billion + return sprintf('%dM', $amount / 1000000.0); + } + + protected function assemble() + { + $this->add([ + Html::tag('li', ['class' => 'object-statistics-graph'], $this->createDonut()), + Html::tag('li', ['class' => ['object-statistics-total', 'text-center']], $this->createTotal()), + Html::tag('li', $this->createBadges()) + ]); + } +} diff --git a/library/Icingadb/Widget/Detail/ObjectsDetail.php b/library/Icingadb/Widget/Detail/ObjectsDetail.php new file mode 100644 index 0000000..a65dfe6 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ObjectsDetail.php @@ -0,0 +1,190 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Chart\Donut; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; +use Icinga\Module\Icingadb\Model\HoststateSummary; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use Icinga\Module\Icingadb\Util\FeatureStatus; +use Icinga\Module\Icingadb\Widget\HostStateBadges; +use Icinga\Module\Icingadb\Widget\ServiceStateBadges; +use ipl\Orm\Query; +use ipl\Stdlib\BaseFilter; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use ipl\Web\Widget\ActionLink; + +class ObjectsDetail extends BaseHtmlElement +{ + use BaseFilter; + + protected $summary; + + protected $query; + + protected $type; + + protected $defaultAttributes = ['class' => 'objects-detail']; + + protected $tag = 'div'; + + /** + * Construct an object detail summary widget + * + * @param string $type + * @param HoststateSummary|ServicestateSummary $summary + * @param Query $query + */ + public function __construct(string $type, $summary, Query $query) + { + $this->summary = $summary; + $this->query = $query; + $this->type = $type; + } + + protected function createChart(): BaseHtmlElement + { + $content = Html::tag('div', ['class' => 'multiselect-summary']); + + if ($this->type === 'host') { + $hostsChart = (new Donut()) + ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']); + + $badges = (new HostStateBadges($this->summary)) + ->setBaseFilter($this->getBaseFilter()); + + $content->add([ + HtmlString::create($hostsChart->render()), + new VerticalKeyValue( + tp('Host', 'Hosts', $this->summary->hosts_total), + $this->summary->hosts_total + ), + $badges + ]); + } else { + $servicesChart = (new Donut()) + ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']); + + $badges = (new ServiceStateBadges($this->summary)) + ->setBaseFilter($this->getBaseFilter()); + + $content->add([ + HtmlString::create($servicesChart->render()), + new VerticalKeyValue( + tp('Service', 'Services', $this->summary->services_total), + $this->summary->services_total + ), + $badges + ]); + } + + return $content; + } + + protected function createComments(): array + { + $content = [Html::tag('h2', t('Comments'))]; + + if ($this->summary->comments_total > 0) { + $content[] = new ActionLink( + sprintf( + tp('Show %d comment', 'Show %d comments', $this->summary->comments_total), + $this->summary->comments_total + ), + Links::comments()->setFilter($this->getBaseFilter()) + ); + } else { + $content[] = new EmptyState(t('No comments created.')); + } + + return $content; + } + + protected function createDowntimes(): array + { + $content = [Html::tag('h2', t('Downtimes'))]; + + if ($this->summary->downtimes_total > 0) { + $content[] = new ActionLink( + sprintf( + tp('Show %d downtime', 'Show %d downtimes', $this->summary->downtimes_total), + $this->summary->downtimes_total + ), + Links::downtimes()->setFilter($this->getBaseFilter()) + ); + } else { + $content[] = new EmptyState(t('No downtimes scheduled.')); + } + + return $content; + } + + protected function createFeatureToggles(): array + { + $form = new ToggleObjectFeaturesForm(new FeatureStatus($this->type, $this->summary)); + + if ($this->type === 'host') { + $form->setAction( + Links::toggleHostsFeatures() + ->setFilter($this->getBaseFilter()) + ->getAbsoluteUrl() + ); + } else { + $form->setAction( + Links::toggleServicesFeatures() + ->setFilter($this->getBaseFilter()) + ->getAbsoluteUrl() + ); + } + + return [ + Html::tag('h2', t('Feature Commands')), + $form + ]; + } + + protected function createExtensions(): array + { + return ObjectsDetailExtensionHook::loadExtensions( + $this->type, + $this->query, + $this->getBaseFilter() + ); + } + + protected function createSummary(): array + { + return [ + Html::tag('h2', t('Summary')), + $this->createChart() + ]; + } + + protected function assemble() + { + $this->add(ObjectsDetailExtensionHook::injectExtensions([ + 190 => $this->createSummary(), + 400 => $this->createComments(), + 401 => $this->createDowntimes(), + 701 => $this->createFeatureToggles() + ], $this->createExtensions())); + } +} diff --git a/library/Icingadb/Widget/Detail/PerfDataTable.php b/library/Icingadb/Widget/Detail/PerfDataTable.php new file mode 100644 index 0000000..aaee7c9 --- /dev/null +++ b/library/Icingadb/Widget/Detail/PerfDataTable.php @@ -0,0 +1,130 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Util\PerfData; +use Icinga\Module\Icingadb\Util\PerfDataSet; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Table; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\Icon; + +class PerfDataTable extends Table +{ + use Translation; + + protected $defaultAttributes = [ + 'class' => 'performance-data-table collapsible', + 'data-visible-rows' => 6 + ]; + + /** @var string The perfdata string */ + protected $perfdataStr; + + /** @var int Max labels to show; 0 for no limit */ + protected $limit; + + /** @var string The color indicating the perfdata state */ + protected $color; + + /** + * Display the given perfdata string to the user + * + * @param string $perfdataStr The perfdata string + * @param int $limit Max labels to show; 0 for no limit + * @param string $color The color indicating the perfdata state + */ + public function __construct(string $perfdataStr, int $limit = 0, string $color = PerfData::PERFDATA_OK) + { + $this->perfdataStr = $perfdataStr; + $this->limit = $limit; + $this->color = $color; + } + + public function assemble() + { + $pieChartData = PerfDataSet::fromString($this->perfdataStr)->asArray(); + $keys = [ + '' => '', + 'label' => t('Label'), + 'value' => t('Value'), + 'min' => t('Min'), + 'max' => t('Max'), + 'warn' => t('Warning'), + 'crit' => t('Critical') + ]; + + $containsSparkline = false; + foreach ($pieChartData as $perfdata) { + if ($perfdata->isVisualizable() || ! $perfdata->isValid()) { + $containsSparkline = true; + break; + } + } + + $headerRow = new HtmlElement('tr'); + foreach ($keys as $key => $col) { + if (! $containsSparkline && $key === '') { + continue; + } + + $headerRow->addHtml(new HtmlElement('th', Attributes::create([ + 'class' => $key === 'label' ? 'title' : null + ]), Text::create($col))); + } + + $this->getHeader()->addHtml($headerRow); + + $count = 0; + foreach ($pieChartData as $perfdata) { + if ($this->limit > 0 && $count === $this->limit) { + break; + } + + $count++; + $cols = []; + if ($containsSparkline) { + if ($perfdata->isVisualizable()) { + $cols[] = Table::td( + HtmlString::create($perfdata->asInlinePie($this->color)->render()), + ['class' => 'sparkline-col'] + ); + } elseif (! $perfdata->isValid()) { + $cols[] = Table::td( + new Icon( + 'triangle-exclamation', + [ + 'title' => $this->translate( + 'Evaluation failed. Performance data is invalid.' + ), + 'class' => ['invalid-perfdata'] + ] + ), + ['class' => 'sparkline-col'] + ); + } else { + $cols[] = Table::td(''); + } + } + + foreach ($perfdata->toArray() as $column => $value) { + $cols[] = Table::td( + new HtmlElement( + 'span', + Attributes::create(['class' => $value ? null : 'no-value']), + $value ? Text::create($value) : new EmptyState(t('None', 'value')) + ), + ['class' => $column === 'label' ? 'title' : null] + ); + } + + $this->addHtml(Table::tr($cols)); + } + } +} diff --git a/library/Icingadb/Widget/Detail/QuickActions.php b/library/Icingadb/Widget/Detail/QuickActions.php new file mode 100644 index 0000000..2ea26c2 --- /dev/null +++ b/library/Icingadb/Widget/Detail/QuickActions.php @@ -0,0 +1,148 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\HostLinks; +use Icinga\Module\Icingadb\Common\ServiceLinks; +use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; +use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Web\Widget\Icon; + +class QuickActions extends BaseHtmlElement +{ + use Auth; + + /** @var Host|Service */ + protected $object; + + protected $tag = 'ul'; + + protected $defaultAttributes = ['class' => 'quick-actions']; + + public function __construct($object) + { + $this->object = $object; + } + + protected function assemble() + { + if ($this->object->state->is_problem) { + if ($this->object->state->is_acknowledged) { + if ($this->isGrantedOn('icingadb/command/remove-acknowledgement', $this->object)) { + $removeAckForm = (new RemoveAcknowledgementForm()) + ->setAction($this->getLink('removeAcknowledgement')) + ->setObjects([$this->object]); + + $this->add(Html::tag('li', $removeAckForm)); + } + } elseif ($this->isGrantedOn('icingadb/command/acknowledge-problem', $this->object)) { + $this->assembleAction( + 'acknowledge', + t('Acknowledge'), + 'check-circle', + t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') + ); + } + } + + if ( + $this->isGrantedOn('icingadb/command/schedule-check', $this->object) + || ( + $this->object->active_checks_enabled + && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $this->object) + ) + ) { + $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); + } + + if ($this->isGrantedOn('icingadb/command/comment/add', $this->object)) { + $this->assembleAction( + 'addComment', + t('Comment', 'verb'), + 'comment', + t('Add a new comment') + ); + } + + if ($this->isGrantedOn('icingadb/command/send-custom-notification', $this->object)) { + $this->assembleAction( + 'sendCustomNotification', + t('Notification'), + 'bell', + t('Send a custom notification') + ); + } + + if ($this->isGrantedOn('icingadb/command/downtime/schedule', $this->object)) { + $this->assembleAction( + 'scheduleDowntime', + t('Downtime'), + 'plug', + t('Schedule a downtime to suppress all problem notifications within a specific period of time') + ); + } + + if ( + $this->isGrantedOn('icingadb/command/schedule-check', $this->object) + || ( + $this->object->active_checks_enabled + && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $this->object) + ) + ) { + $this->assembleAction( + 'scheduleCheck', + t('Reschedule'), + 'calendar', + t('Schedule the next active check at a different time than the current one') + ); + } + + if ($this->isGrantedOn('icingadb/command/process-check-result', $this->object)) { + $this->assembleAction( + 'processCheckresult', + t('Process check result'), + 'edit', + sprintf( + t('Submit a one time or so called passive result for the %s check'), + $this->object->checkcommand_name + ) + ); + } + } + + protected function assembleAction(string $action, string $label, string $icon, string $title) + { + $link = Html::tag( + 'a', + [ + 'href' => $this->getLink($action), + 'class' => 'action-link', + 'title' => $title, + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ], + [ + new Icon($icon), + $label + ] + ); + + $this->add(Html::tag('li', $link)); + } + + protected function getLink($action) + { + if ($this->object instanceof Host) { + return HostLinks::$action($this->object)->getAbsoluteUrl(); + } else { + return ServiceLinks::$action($this->object, $this->object->host)->getAbsoluteUrl(); + } + } +} diff --git a/library/Icingadb/Widget/Detail/ServiceDetail.php b/library/Icingadb/Widget/Detail/ServiceDetail.php new file mode 100644 index 0000000..8421e31 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ServiceDetail.php @@ -0,0 +1,37 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Service; + +class ServiceDetail extends ObjectDetail +{ + public function __construct(Service $object) + { + parent::__construct($object); + } + + protected function assemble() + { + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + $this->add($this->createPrintHeader()); + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 0 => $this->createPluginOutput(), + 300 => $this->createActions(), + 301 => $this->createNotes(), + 400 => $this->createComments(), + 401 => $this->createDowntimes(), + 500 => $this->createGroups(), + 501 => $this->createNotifications(), + 600 => $this->createCheckStatistics(), + 601 => $this->createPerformanceData(), + 700 => $this->createCustomVars(), + 701 => $this->createFeatureToggles() + ], $this->createExtensions())); + } +} diff --git a/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php b/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php new file mode 100644 index 0000000..f29ee9b --- /dev/null +++ b/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\ObjectInspectionDetail; + +class ServiceInspectionDetail extends ObjectInspectionDetail +{ + protected function assemble() + { + $this->add([ + $this->createSourceLocation(), + $this->createLastCheckResult(), + $this->createAttributes(), + $this->createCustomVariables(), + $this->createRedisInfo() + ]); + } +} diff --git a/library/Icingadb/Widget/Detail/ServiceMetaInfo.php b/library/Icingadb/Widget/Detail/ServiceMetaInfo.php new file mode 100644 index 0000000..cca7237 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ServiceMetaInfo.php @@ -0,0 +1,61 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; + +class ServiceMetaInfo extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'object-meta-info']; + + /** @var Service */ + protected $service; + + public function __construct(Service $service) + { + $this->service = $service; + } + + protected function assemble() + { + $this->addHtml( + new VerticalKeyValue('service.name', $this->service->name), + new VerticalKeyValue( + 'last_state_change', + DateFormatter::formatDateTime($this->service->state->last_state_change->getTimestamp()) + ) + ); + + $collapsible = new HtmlElement('div', Attributes::create([ + 'class' => 'collapsible', + 'id' => 'object-meta-info', + 'data-toggle-element' => '.object-meta-info-control', + 'data-visible-height' => 0 + ])); + + $renderHelper = new HtmlDocument(); + $renderHelper->addHtml( + $this, + new HtmlElement( + 'button', + Attributes::create(['class' => 'object-meta-info-control']), + new Icon('angle-double-up', ['class' => 'collapse-icon']), + new Icon('angle-double-down', ['class' => 'expand-icon']) + ) + ); + + $this->addWrapper($collapsible); + $this->addWrapper($renderHelper); + } +} diff --git a/library/Icingadb/Widget/Detail/ServiceStatistics.php b/library/Icingadb/Widget/Detail/ServiceStatistics.php new file mode 100644 index 0000000..51aced1 --- /dev/null +++ b/library/Icingadb/Widget/Detail/ServiceStatistics.php @@ -0,0 +1,64 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Chart\Donut; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Widget\ServiceStateBadges; +use ipl\Html\ValidHtml; +use ipl\Web\Widget\VerticalKeyValue; +use ipl\Html\HtmlString; +use ipl\Web\Widget\Link; + +class ServiceStatistics extends ObjectStatistics +{ + protected $summary; + + public function __construct($summary) + { + $this->summary = $summary; + } + + protected function createDonut(): ValidHtml + { + $donut = (new Donut()) + ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']); + + return HtmlString::create($donut->render()); + } + + protected function createTotal(): ValidHtml + { + $url = Links::services(); + if ($this->hasBaseFilter()) { + $url->setFilter($this->getBaseFilter()); + } + + return new Link( + (new VerticalKeyValue( + tp('Service', 'Services', $this->summary->services_total), + $this->shortenAmount($this->summary->services_total) + ))->setAttribute('title', $this->summary->services_total), + $url + ); + } + + protected function createBadges(): ValidHtml + { + $badges = new ServiceStateBadges($this->summary); + if ($this->hasBaseFilter()) { + $badges->setBaseFilter($this->getBaseFilter()); + } + + return $badges; + } +} diff --git a/library/Icingadb/Widget/Detail/UserDetail.php b/library/Icingadb/Widget/Detail/UserDetail.php new file mode 100644 index 0000000..0bc1acb --- /dev/null +++ b/library/Icingadb/Widget/Detail/UserDetail.php @@ -0,0 +1,188 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\User; +use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Html\Attributes; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; +use Icinga\Module\Icingadb\Widget\ItemTable\UsergroupTable; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; + +class UserDetail extends BaseHtmlElement +{ + use Auth; + use Database; + + /** @var User The given user */ + protected $user; + + protected $defaultAttributes = ['class' => 'object-detail']; + + protected $tag = 'div'; + + public function __construct(User $user) + { + $this->user = $user; + } + + protected function createCustomVars(): array + { + $content = [new HtmlElement('h2', null, Text::create(t('Custom Variables')))]; + $flattenedVars = $this->user->customvar_flat; + $this->applyRestrictions($flattenedVars); + + $vars = $this->user->customvar_flat->getModel()->unflattenVars($flattenedVars); + if (! empty($vars)) { + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => 'user-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->user)); + } else { + $content[] = new EmptyState(t('No custom variables configured.')); + } + + return $content; + } + + protected function createUserDetail(): array + { + list($hostStates, $serviceStates) = $this->separateStates($this->user->states); + $hostStates = implode(', ', $this->localizeStates($hostStates)); + $serviceStates = implode(', ', $this->localizeStates($serviceStates)); + $types = implode(', ', $this->localizeTypes($this->user->types)); + + return [ + new HtmlElement('h2', null, Text::create(t('Details'))), + new HorizontalKeyValue(t('Name'), $this->user->name), + new HorizontalKeyValue(t('E-Mail'), $this->user->email ?: new EmptyState(t('None', 'address'))), + new HorizontalKeyValue(t('Pager'), $this->user->pager ?: new EmptyState(t('None', 'phone-number'))), + new HorizontalKeyValue(t('Host States'), $hostStates ?: t('All')), + new HorizontalKeyValue(t('Service States'), $serviceStates ?: t('All')), + new HorizontalKeyValue(t('Types'), $types ?: t('All')) + ]; + } + + protected function createUsergroupList(): array + { + $userGroups = $this->user->usergroup->limit(6)->peekAhead()->execute(); + + $showMoreLink = (new ShowMore( + $userGroups, + Links::usergroups()->addParams(['user.name' => $this->user->name]) + ))->setBaseTarget('_next'); + + return [ + new HtmlElement('h2', null, Text::create(t('Groups'))), + new UsergroupTable($userGroups), + $showMoreLink + ]; + } + + protected function createExtensions(): array + { + return ObjectDetailExtensionHook::loadExtensions($this->user); + } + + protected function assemble() + { + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 200 => $this->createUserDetail(), + 500 => $this->createUsergroupList(), + 700 => $this->createCustomVars() + ], $this->createExtensions())); + } + + private function localizeTypes(array $types): array + { + $localizedTypes = []; + foreach ($types as $type) { + switch ($type) { + case 'problem': + $localizedTypes[] = t('Problem'); + break; + case 'ack': + $localizedTypes[] = t('Acknowledgement'); + break; + case 'recovery': + $localizedTypes[] = t('Recovery'); + break; + case 'downtime_start': + $localizedTypes[] = t('Downtime Start'); + break; + case 'downtime_end': + $localizedTypes[] = t('Downtime End'); + break; + case 'downtime_removed': + $localizedTypes[] = t('Downtime Removed'); + break; + case 'flapping_start': + $localizedTypes[] = t('Flapping Start'); + break; + case 'flapping_end': + $localizedTypes[] = t('Flapping End'); + break; + case 'custom': + $localizedTypes[] = t('Custom'); + break; + } + } + + return $localizedTypes; + } + + private function localizeStates(array $states): array + { + $localizedState = []; + foreach ($states as $state) { + switch ($state) { + case 'up': + $localizedState[] = t('Up'); + break; + case 'down': + $localizedState[] = t('Down'); + break; + case 'ok': + $localizedState[] = t('Ok'); + break; + case 'warning': + $localizedState[] = t('Warning'); + break; + case 'critical': + $localizedState[] = t('Critical'); + break; + case 'unknown': + $localizedState[] = t('Unknown'); + break; + } + } + + return $localizedState; + } + + private function separateStates(array $states): array + { + $hostStates = []; + $serviceStates = []; + + foreach ($states as $state) { + if ($state === 'Up' || $state === 'Down') { + $hostStates[] = $state; + } else { + $serviceStates[] = $state; + } + } + + return [$hostStates, $serviceStates]; + } +} diff --git a/library/Icingadb/Widget/Detail/UsergroupDetail.php b/library/Icingadb/Widget/Detail/UsergroupDetail.php new file mode 100644 index 0000000..249c795 --- /dev/null +++ b/library/Icingadb/Widget/Detail/UsergroupDetail.php @@ -0,0 +1,98 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\Detail; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; +use Icinga\Module\Icingadb\Model\Usergroup; +use Icinga\Module\Icingadb\Widget\ItemTable\UserTable; +use Icinga\Module\Icingadb\Widget\ShowMore; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\HorizontalKeyValue; + +class UsergroupDetail extends BaseHtmlElement +{ + use Auth; + use Database; + + /** @var Usergroup The given user group */ + protected $usergroup; + + protected $defaultAttributes = ['class' => 'object-detail']; + + protected $tag = 'div'; + + public function __construct(Usergroup $usergroup) + { + $this->usergroup = $usergroup; + } + + protected function createPrintHeader() + { + return [ + new HtmlElement('h2', null, Text::create(t('Details'))), + new HorizontalKeyValue(t('Name'), $this->usergroup->name) + ]; + } + + protected function createCustomVars(): array + { + $content = [new HtmlElement('h2', null, Text::create(t('Custom Variables')))]; + $flattenedVars = $this->usergroup->customvar_flat; + $this->applyRestrictions($flattenedVars); + + $vars = $this->usergroup->customvar_flat->getModel()->unflattenVars($flattenedVars); + if (! empty($vars)) { + $content[] = new HtmlElement('div', Attributes::create([ + 'id' => 'usergroup-customvars', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]), new CustomVarTable($vars, $this->usergroup)); + } else { + $content[] = new EmptyState(t('No custom variables configured.')); + } + + return $content; + } + + protected function createUserList(): array + { + $users = $this->usergroup->user->limit(6)->peekAhead()->execute(); + + $showMoreLink = (new ShowMore( + $users, + Links::users()->addParams(['usergroup.name' => $this->usergroup->name]) + ))->setBaseTarget('_next'); + + return [ + new HtmlElement('h2', null, Text::create(t('Users'))), + new UserTable($users), + $showMoreLink + ]; + } + + protected function createExtensions(): array + { + return ObjectDetailExtensionHook::loadExtensions($this->usergroup); + } + + protected function assemble() + { + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + $this->add($this->createPrintHeader()); + } + + $this->add(ObjectDetailExtensionHook::injectExtensions([ + 500 => $this->createUserList(), + 700 => $this->createCustomVars() + ], $this->createExtensions())); + } +} diff --git a/library/Icingadb/Widget/Health.php b/library/Icingadb/Widget/Health.php new file mode 100644 index 0000000..8c99dca --- /dev/null +++ b/library/Icingadb/Widget/Health.php @@ -0,0 +1,66 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Web\Widget\TimeAgo; +use ipl\Web\Widget\TimeSince; +use ipl\Web\Widget\VerticalKeyValue; + +class Health extends BaseHtmlElement +{ + protected $data; + + protected $tag = 'section'; + + public function __construct($data) + { + $this->data = $data; + } + + protected function assemble() + { + if ($this->data->heartbeat->getTimestamp() > time() - 60) { + $this->add(Html::tag('div', ['class' => 'icinga-health up'], [ + Html::sprintf( + t('Icinga 2 is up and running %s', '...since <timespan>'), + new TimeSince($this->data->icinga2_start_time->getTimestamp()) + ) + ])); + } else { + $this->add(Html::tag('div', ['class' => 'icinga-health down'], [ + Html::sprintf( + t('Icinga 2 or Icinga DB is not running %s', '...since <timespan>'), + new TimeSince($this->data->heartbeat->getTimestamp()) + ) + ])); + } + + $icingaInfo = Html::tag('div', ['class' => 'icinga-info'], [ + new VerticalKeyValue( + t('Icinga 2 Version'), + $this->data->icinga2_version + ), + new VerticalKeyValue( + t('Icinga 2 Start Time'), + new TimeAgo($this->data->icinga2_start_time->getTimestamp()) + ), + new VerticalKeyValue( + t('Last Heartbeat'), + new TimeAgo($this->data->heartbeat->getTimestamp()) + ), + new VerticalKeyValue( + t('Active Icinga 2 Endpoint'), + $this->data->endpoint->name ?: t('N/A') + ), + new VerticalKeyValue( + t('Active Icinga Web Endpoint'), + gethostname() ?: t('N/A') + ) + ]); + $this->add($icingaInfo); + } +} diff --git a/library/Icingadb/Widget/HostStateBadges.php b/library/Icingadb/Widget/HostStateBadges.php new file mode 100644 index 0000000..8141e82 --- /dev/null +++ b/library/Icingadb/Widget/HostStateBadges.php @@ -0,0 +1,45 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\StateBadges; +use ipl\Web\Url; + +class HostStateBadges extends StateBadges +{ + protected function getBaseUrl(): Url + { + return Links::hosts(); + } + + protected function getType(): string + { + return 'host'; + } + + protected function getPrefix(): string + { + return 'hosts'; + } + + protected function getStateInt(string $state): int + { + return HostStates::int($state); + } + + protected function assemble() + { + $this->addAttributes(['class' => 'host-state-badges']); + + $this->add(array_filter([ + $this->createGroup('down'), + $this->createBadge('unknown'), + $this->createBadge('up'), + $this->createBadge('pending') + ])); + } +} diff --git a/library/Icingadb/Widget/HostStatusBar.php b/library/Icingadb/Widget/HostStatusBar.php new file mode 100644 index 0000000..f900f76 --- /dev/null +++ b/library/Icingadb/Widget/HostStatusBar.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Module\Icingadb\Common\BaseStatusBar; +use ipl\Html\BaseHtmlElement; + +class HostStatusBar extends BaseStatusBar +{ + protected function assembleTotal(BaseHtmlElement $total): void + { + $total->add(sprintf(tp('%d Host', '%d Hosts', $this->summary->hosts_total), $this->summary->hosts_total)); + } + + protected function createStateBadges(): BaseHtmlElement + { + return (new HostStateBadges($this->summary))->setBaseFilter($this->getBaseFilter()); + } +} diff --git a/library/Icingadb/Widget/HostSummaryDonut.php b/library/Icingadb/Widget/HostSummaryDonut.php new file mode 100644 index 0000000..db5fef8 --- /dev/null +++ b/library/Icingadb/Widget/HostSummaryDonut.php @@ -0,0 +1,78 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Chart\Donut; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\HoststateSummary; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Stdlib\BaseFilter; +use ipl\Stdlib\Filter; +use ipl\Web\Common\Card; +use ipl\Web\Filter\QueryString; + +class HostSummaryDonut extends Card +{ + use BaseFilter; + + protected $defaultAttributes = ['class' => 'donut-container', 'data-base-target' => '_next']; + + /** @var HoststateSummary */ + protected $summary; + + public function __construct(HoststateSummary $summary) + { + $this->summary = $summary; + } + + protected function assembleBody(BaseHtmlElement $body) + { + $labelBigUrlFilter = Filter::all( + Filter::equal('host.state.soft_state', 1), + Filter::equal('host.state.is_handled', 'n') + ); + if ($this->hasBaseFilter()) { + $labelBigUrlFilter->add($this->getBaseFilter()); + } + + $donut = (new Donut()) + ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']) + ->setLabelBig($this->summary->hosts_down_unhandled) + ->setLabelBigUrl(Links::hosts()->setFilter($labelBigUrlFilter)->addParams([ + 'sort' => 'host.state.last_state_change' + ])) + ->setLabelBigEyeCatching($this->summary->hosts_down_unhandled > 0) + ->setLabelSmall(t('Down')); + + $body->addHtml( + new HtmlElement('div', Attributes::create(['class' => 'donut']), new HtmlString($donut->render())) + ); + } + + protected function assembleFooter(BaseHtmlElement $footer) + { + $footer->addHtml((new HostStateBadges($this->summary))->setBaseFilter($this->getBaseFilter())); + } + + protected function assembleHeader(BaseHtmlElement $header) + { + $header->addHtml( + new HtmlElement('h2', null, Text::create(t('Hosts'))), + new HtmlElement('span', Attributes::create(['class' => 'meta']), TemplateString::create( + t('{{#total}}Total{{/total}} %d'), + ['total' => new HtmlElement('span')], + (int) $this->summary->hosts_total + )) + ); + } +} diff --git a/library/Icingadb/Widget/IconImage.php b/library/Icingadb/Widget/IconImage.php new file mode 100644 index 0000000..fcf25c8 --- /dev/null +++ b/library/Icingadb/Widget/IconImage.php @@ -0,0 +1,74 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Web\Url; +use ipl\Web\Widget\Icon; + +class IconImage extends BaseHtmlElement +{ + /** @var string */ + protected $source; + + /** @var ?string */ + protected $alt; + + protected $tag = 'img'; + + /** + * Create a new icon image + * + * @param string $source + * @param ?string $alt The alternative text + */ + public function __construct(string $source, ?string $alt) + { + $this->source = $source; + $this->alt = $alt; + } + + public function renderUnwrapped() + { + if (! $this->getAttributes()->has('src')) { + // If it's an icon we don't need the <img> tag + return ''; + } + + return parent::renderUnwrapped(); + } + + protected function assemble() + { + $src = $this->source; + + if (strpos($src, '.') === false) { + $this->setWrapper((new HtmlDocument())->addHtml(new Icon($src))); + return; + } + + if (strpos($src, '/') === false) { + $src = 'img/icons/' . $src; + } + + if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { + $srcUrl = Url::fromPath($src); + $srcPath = $srcUrl->getRelativeUrl(); + if (! $srcUrl->isExternal() && file_exists($srcPath) && is_file($srcPath)) { + $mimeType = @mime_content_type($srcPath); + $content = @file_get_contents($srcPath); + if ($mimeType !== false && $content !== false) { + $src = "data:$mimeType;base64," . base64_encode($content); + } + } + } + + $this->addAttributes([ + 'src' => $src, + 'alt' => $this->alt + ]); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseCommentListItem.php b/library/Icingadb/Widget/ItemList/BaseCommentListItem.php new file mode 100644 index 0000000..de11c0c --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseCommentListItem.php @@ -0,0 +1,131 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ObjectLinkDisabled; +use Icinga\Module\Icingadb\Common\TicketLinks; +use ipl\Html\Html; +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Widget\MarkdownLine; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Model\Comment; +use ipl\Html\FormattedString; +use ipl\Web\Common\BaseListItem; +use ipl\Web\Widget\TimeAgo; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\TimeUntil; + +/** + * Comment item of a comment list. Represents one database row. + * + * @property Comment $item + * @property CommentList $list + */ +abstract class BaseCommentListItem extends BaseListItem +{ + use HostLink; + use ServiceLink; + use NoSubjectLink; + use ObjectLinkDisabled; + use TicketLinks; + + protected function assembleCaption(BaseHtmlElement $caption): void + { + $markdownLine = new MarkdownLine($this->createTicketLinks($this->item->text)); + + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->addFrom($markdownLine); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $isAck = $this->item->entry_type === 'ack'; + $expires = $this->item->expire_time; + + $subjectText = sprintf( + $isAck ? t('%s acknowledged', '<username>..') : t('%s commented', '<username>..'), + $this->item->author + ); + + $headerParts = [ + new Icon(Icons::USER), + $this->getNoSubjectLink() + ? new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($subjectText)) + : new Link($subjectText, Links::comment($this->item), ['class' => 'subject']) + ]; + + if ($isAck) { + $label = [Text::create('ack')]; + + if ($this->item->is_persistent) { + array_unshift($label, new Icon(Icons::IS_PERSISTENT)); + } + + $headerParts[] = Text::create(' '); + $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge badge']), ...$label); + } + + if ($expires !== null) { + $headerParts[] = Text::create(' '); + $headerParts[] = new HtmlElement( + 'span', + Attributes::create(['class' => 'ack-badge badge']), + Text::create(t('EXPIRES')) + ); + } + + if ($this->getObjectLinkDisabled()) { + // pass + } elseif ($this->item->object_type === 'host') { + $headerParts[] = $this->createHostLink($this->item->host, true); + } else { + $headerParts[] = $this->createServiceLink($this->item->service, $this->item->service->host, true); + } + + $title->addHtml(...$headerParts); + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $visual->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'user-ball']), + Text::create($this->item->author[0]) + )); + } + + protected function createTimestamp(): ?BaseHtmlElement + { + if ($this->item->expire_time) { + return Html::tag( + 'span', + FormattedString::create(t("expires %s"), new TimeUntil($this->item->expire_time->getTimestamp())) + ); + } + + return Html::tag( + 'span', + FormattedString::create(t("created %s"), new TimeAgo($this->item->entry_time->getTimestamp())) + ); + } + + protected function init(): void + { + $this->setTicketLinkEnabled($this->list->getTicketLinkEnabled()); + $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + $this->list->addMultiselectFilterAttribute($this, Filter::equal('name', $this->item->name)); + $this->setObjectLinkDisabled($this->list->getObjectLinkDisabled()); + $this->setNoSubjectLink($this->list->getNoSubjectLink()); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseDowntimeListItem.php b/library/Icingadb/Widget/ItemList/BaseDowntimeListItem.php new file mode 100644 index 0000000..7ebc1f6 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseDowntimeListItem.php @@ -0,0 +1,212 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ObjectLinkDisabled; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Model\Downtime; +use Icinga\Module\Icingadb\Widget\MarkdownLine; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseListItem; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; + +/** + * Downtime item of a downtime list. Represents one database row. + * + * @property Downtime $item + * @property DowntimeList $list + */ +abstract class BaseDowntimeListItem extends BaseListItem +{ + use HostLink; + use ServiceLink; + use NoSubjectLink; + use ObjectLinkDisabled; + use TicketLinks; + + /** @var int Current Time */ + protected $currentTime; + + /** @var int Duration */ + protected $duration; + + /** @var int Downtime end time */ + protected $endTime; + + /** @var bool Whether the downtime is active */ + protected $isActive; + + /** @var int Downtime start time */ + protected $startTime; + + protected function init(): void + { + if ($this->item->is_flexible && $this->item->is_in_effect) { + $this->startTime = $this->item->start_time->getTimestamp(); + $this->endTime = $this->item->end_time->getTimestamp(); + } else { + $this->startTime = $this->item->scheduled_start_time->getTimestamp(); + $this->endTime = $this->item->scheduled_end_time->getTimestamp(); + } + + $this->currentTime = time(); + + $this->isActive = $this->item->is_in_effect + || $this->item->is_flexible && $this->item->scheduled_start_time->getTimestamp() <= $this->currentTime; + + $until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime; + $this->duration = explode(' ', DateFormatter::formatDuration( + $until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600)) + ), 2)[0]; + + $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + $this->list->addMultiselectFilterAttribute($this, Filter::equal('name', $this->item->name)); + $this->setObjectLinkDisabled($this->list->getObjectLinkDisabled()); + $this->setNoSubjectLink($this->list->getNoSubjectLink()); + $this->setTicketLinkEnabled($this->list->getTicketLinkEnabled()); + + if ($this->item->is_in_effect) { + $this->getAttributes()->add('class', 'in-effect'); + } + } + + protected function createProgress(): BaseHtmlElement + { + return new HtmlElement( + 'div', + Attributes::create([ + 'class' => 'progress', + 'data-animate-progress' => true, + 'data-start-time' => $this->startTime, + 'data-end-time' => $this->endTime + ]), + new HtmlElement( + 'div', + Attributes::create(['class' => 'bar']) + ) + ); + } + + protected function assembleCaption(BaseHtmlElement $caption): void + { + $markdownLine = new MarkdownLine($this->createTicketLinks($this->item->comment)); + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->addHtml( + new HtmlElement( + 'span', + null, + new Icon(Icons::USER), + Text::create($this->item->author) + ), + Text::create(': ') + )->addFrom($markdownLine); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + if ($this->getObjectLinkDisabled()) { + $link = null; + } elseif ($this->item->object_type === 'host') { + $link = $this->createHostLink($this->item->host, true); + } else { + $link = $this->createServiceLink($this->item->service, $this->item->service->host, true); + } + + if ($this->item->is_flexible) { + if ($link !== null) { + $template = t('{{#link}}Flexible Downtime{{/link}} for %s'); + } else { + $template = t('Flexible Downtime'); + } + } else { + if ($link !== null) { + $template = t('{{#link}}Fixed Downtime{{/link}} for %s'); + } else { + $template = t('Fixed Downtime'); + } + } + + if ($this->getNoSubjectLink()) { + if ($link === null) { + $title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template)); + } else { + $title->addHtml(TemplateString::create( + $template, + ['link' => HtmlElement::create('span', [ 'class' => 'subject'])], + $link + )); + } + } else { + if ($link === null) { + $title->addHtml(new Link($template, Links::downtime($this->item))); + } else { + $title->addHtml(TemplateString::create( + $template, + ['link' => new Link('', Links::downtime($this->item))], + $link + )); + } + } + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $dateTime = DateFormatter::formatDateTime($this->endTime); + + if ($this->isActive) { + $visual->addHtml(Html::sprintf( + t('%s left', '<timespan>..'), + Html::tag( + 'strong', + Html::tag( + 'time', + [ + 'datetime' => $dateTime, + 'title' => $dateTime + ], + $this->duration + ) + ) + )); + } else { + $visual->addHtml(Html::sprintf( + t('in %s', '..<timespan>'), + Html::tag('strong', $this->duration) + )); + } + } + + protected function createTimestamp(): ?BaseHtmlElement + { + $dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime); + + return Html::tag( + 'time', + [ + 'datetime' => $dateTime, + 'title' => $dateTime + ], + sprintf( + $this->isActive + ? t('expires in %s', '..<timespan>') + : t('starts in %s', '..<timespan>'), + $this->duration + ) + ); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseHistoryListItem.php b/library/Icingadb/Widget/ItemList/BaseHistoryListItem.php new file mode 100644 index 0000000..6999324 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseHistoryListItem.php @@ -0,0 +1,405 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Widget\MarkdownLine; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Model\History; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\CheckAttempt; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use Icinga\Module\Icingadb\Widget\StateChange; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseListItem; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\StateBall; +use ipl\Web\Widget\TimeAgo; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; + +abstract class BaseHistoryListItem extends BaseListItem +{ + use HostLink; + use NoSubjectLink; + use ServiceLink; + use TicketLinks; + + /** @var History */ + protected $item; + + /** @var HistoryList */ + protected $list; + + protected function init(): void + { + $this->setNoSubjectLink($this->list->getNoSubjectLink()); + $this->setTicketLinkEnabled($this->list->getTicketLinkEnabled()); + $this->list->addDetailFilterAttribute($this, Filter::equal('id', bin2hex($this->item->id))); + } + + abstract protected function getStateBallSize(): string; + + protected function assembleCaption(BaseHtmlElement $caption): void + { + switch ($this->item->event_type) { + case 'comment_add': + case 'comment_remove': + $markdownLine = new MarkdownLine($this->createTicketLinks($this->item->comment->comment)); + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->add([ + new Icon(Icons::USER), + $this->item->comment->author, + ': ' + ])->addFrom($markdownLine); + + break; + case 'downtime_end': + case 'downtime_start': + $markdownLine = new MarkdownLine($this->createTicketLinks($this->item->downtime->comment)); + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->add([ + new Icon(Icons::USER), + $this->item->downtime->author, + ': ' + ])->addFrom($markdownLine); + + break; + case 'flapping_start': + $caption + ->add(sprintf( + t('State Change Rate: %.2f%%; Start Threshold: %.2f%%'), + $this->item->flapping->percent_state_change_start, + $this->item->flapping->flapping_threshold_high + )) + ->getAttributes() + ->add('class', 'plugin-output'); + + break; + case 'flapping_end': + $caption + ->add(sprintf( + t('State Change Rate: %.2f%%; End Threshold: %.2f%%; Flapping for %s'), + $this->item->flapping->percent_state_change_end, + $this->item->flapping->flapping_threshold_low, + DateFormatter::formatDuration( + $this->item->flapping->end_time->getTimestamp() + - $this->item->flapping->start_time->getTimestamp() + ) + )) + ->getAttributes() + ->add('class', 'plugin-output'); + + break; + case 'ack_clear': + case 'ack_set': + if (! isset($this->item->acknowledgement->comment) && ! isset($this->item->acknowledgement->author)) { + $caption->addHtml(new EmptyState( + t('This acknowledgement was set before Icinga DB history recording') + )); + } else { + $markdownLine = new MarkdownLine($this->createTicketLinks($this->item->acknowledgement->comment)); + $caption->getAttributes()->add($markdownLine->getAttributes()); + $caption->add([ + new Icon(Icons::USER), + $this->item->acknowledgement->author, + ': ' + ])->addFrom($markdownLine); + } + + break; + case 'notification': + if (! empty($this->item->notification->author)) { + $caption->add([ + new Icon(Icons::USER), + $this->item->notification->author, + ': ', + $this->item->notification->text + ]); + } else { + $commandName = $this->item->object_type === 'host' + ? $this->item->host->checkcommand_name + : $this->item->service->checkcommand_name; + if (isset($commandName)) { + if (empty($this->item->notification->text)) { + $caption->addHtml(new EmptyState(t('Output unavailable.'))); + } else { + $caption->addHtml(new PluginOutputContainer( + (new PluginOutput($this->item->notification->text)) + ->setCommandName($commandName) + )); + } + } else { + $caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.'))); + } + } + + break; + case 'state_change': + $commandName = $this->item->object_type === 'host' + ? $this->item->host->checkcommand_name + : $this->item->service->checkcommand_name; + if (isset($commandName)) { + if (empty($this->item->state->output)) { + $caption->addHtml(new EmptyState(t('Output unavailable.'))); + } else { + $caption->addHtml(new PluginOutputContainer( + (new PluginOutput($this->item->state->output)) + ->setCommandName($commandName) + )); + } + } else { + $caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.'))); + } + + break; + } + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + switch ($this->item->event_type) { + case 'comment_add': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::COMMENT) + )); + + break; + case 'comment_remove': + case 'downtime_end': + case 'ack_clear': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::REMOVE) + )); + + break; + case 'downtime_start': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IN_DOWNTIME) + )); + + break; + case 'ack_set': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IS_ACKNOWLEDGED) + )); + + break; + case 'flapping_end': + case 'flapping_start': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IS_FLAPPING) + )); + + break; + case 'notification': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::NOTIFICATION) + )); + + break; + case 'state_change': + if ($this->item->state->state_type === 'soft') { + $stateType = 'soft_state'; + $previousStateType = 'previous_soft_state'; + + if ($this->item->state->previous_soft_state === 0) { + $previousStateType = 'hard_state'; + } + + $visual->addHtml(new CheckAttempt( + (int) $this->item->state->check_attempt, + (int) $this->item->state->max_check_attempts + )); + } else { + $stateType = 'hard_state'; + $previousStateType = 'previous_hard_state'; + + if ($this->item->state->hard_state === $this->item->state->previous_hard_state) { + $previousStateType = 'previous_soft_state'; + } + } + + if ($this->item->object_type === 'host') { + $state = HostStates::text($this->item->state->$stateType); + $previousState = HostStates::text($this->item->state->$previousStateType); + } else { + $state = ServiceStates::text($this->item->state->$stateType); + $previousState = ServiceStates::text($this->item->state->$previousStateType); + } + + $stateChange = new StateChange($state, $previousState); + if ($stateType === 'soft_state') { + $stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + } + + if ($previousStateType === 'previous_soft_state') { + $stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + if ($stateType === 'soft_state') { + $visual->getAttributes()->add('class', 'small-state-change'); + } + } + + $visual->prependHtml($stateChange); + + break; + } + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + switch ($this->item->event_type) { + case 'comment_add': + $subjectLabel = t('Comment added'); + + break; + case 'comment_remove': + if (! empty($this->item->comment->removed_by)) { + if ($this->item->comment->removed_by !== $this->item->comment->author) { + $subjectLabel = sprintf( + t('Comment removed by %s', '..<username>'), + $this->item->comment->removed_by + ); + } else { + $subjectLabel = t('Comment removed by author'); + } + } elseif (isset($this->item->comment->expire_time)) { + $subjectLabel = t('Comment expired'); + } else { + $subjectLabel = t('Comment removed'); + } + + break; + case 'downtime_end': + if (! empty($this->item->downtime->cancelled_by)) { + if ($this->item->downtime->cancelled_by !== $this->item->downtime->author) { + $subjectLabel = sprintf( + t('Downtime cancelled by %s', '..<username>'), + $this->item->downtime->cancelled_by + ); + } else { + $subjectLabel = t('Downtime cancelled by author'); + } + } elseif ($this->item->downtime->has_been_cancelled === 'y') { + $subjectLabel = t('Downtime cancelled'); + } else { + $subjectLabel = t('Downtime ended'); + } + + break; + case 'downtime_start': + $subjectLabel = t('Downtime started'); + + break; + case 'flapping_start': + $subjectLabel = t('Flapping started'); + + break; + case 'flapping_end': + $subjectLabel = t('Flapping stopped'); + + break; + case 'ack_set': + $subjectLabel = t('Acknowledgement set'); + + break; + case 'ack_clear': + if (! empty($this->item->acknowledgement->cleared_by)) { + if ($this->item->acknowledgement->cleared_by !== $this->item->acknowledgement->author) { + $subjectLabel = sprintf( + t('Acknowledgement cleared by %s', '..<username>'), + $this->item->acknowledgement->cleared_by + ); + } else { + $subjectLabel = t('Acknowledgement cleared by author'); + } + } elseif (isset($this->item->acknowledgement->expire_time)) { + $subjectLabel = t('Acknowledgement expired'); + } else { + $subjectLabel = t('Acknowledgement cleared'); + } + + break; + case 'notification': + $subjectLabel = sprintf( + NotificationListItem::phraseForType($this->item->notification->type), + ucfirst($this->item->object_type) + ); + + break; + case 'state_change': + $state = $this->item->state->state_type === 'hard' + ? $this->item->state->hard_state + : $this->item->state->soft_state; + if ($state === 0) { + if ($this->item->object_type === 'service') { + $subjectLabel = t('Service recovered'); + } else { + $subjectLabel = t('Host recovered'); + } + } else { + if ($this->item->state->state_type === 'hard') { + $subjectLabel = t('Hard state changed'); + } else { + $subjectLabel = t('Soft state changed'); + } + } + + break; + default: + $subjectLabel = $this->item->event_type; + + break; + } + + if ($this->getNoSubjectLink()) { + $title->addHtml(HtmlElement::create('span', ['class' => 'subject'], $subjectLabel)); + } else { + $title->addHtml(new Link($subjectLabel, Links::event($this->item), ['class' => 'subject'])); + } + + if ($this->item->object_type === 'host') { + if (isset($this->item->host->id)) { + $link = $this->createHostLink($this->item->host, true); + } + } else { + if (isset($this->item->host->id, $this->item->service->id)) { + $link = $this->createServiceLink($this->item->service, $this->item->host, true); + } + } + + $title->addHtml(Text::create(' ')); + if (isset($link)) { + $title->addHtml($link); + } + } + + protected function createTimestamp(): ?BaseHtmlElement + { + return new TimeAgo($this->item->event_time->getTimestamp()); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseHostListItem.php b/library/Icingadb/Widget/ItemList/BaseHostListItem.php new file mode 100644 index 0000000..edaf6c8 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseHostListItem.php @@ -0,0 +1,56 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Link; + +/** + * Host item of a host list. Represents one database row. + * + * @property Host $item + * @property HostList $list + */ +abstract class BaseHostListItem extends StateListItem +{ + use NoSubjectLink; + + /** + * Create new subject link + * + * @return BaseHtmlElement + */ + protected function createSubject() + { + if ($this->getNoSubjectLink()) { + return new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ); + } else { + return new Link($this->item->display_name, Links::host($this->item), ['class' => 'subject']); + } + } + + protected function init(): void + { + parent::init(); + + if ($this->list->getNoSubjectLink()) { + $this->setNoSubjectLink(); + } + + $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)) + ->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name)); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseNotificationListItem.php b/library/Icingadb/Widget/ItemList/BaseNotificationListItem.php new file mode 100644 index 0000000..b538ac4 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseNotificationListItem.php @@ -0,0 +1,189 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\HostLink; +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ServiceLink; +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use Icinga\Module\Icingadb\Widget\StateChange; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseListItem; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\TimeAgo; +use InvalidArgumentException; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; + +abstract class BaseNotificationListItem extends BaseListItem +{ + use HostLink; + use NoSubjectLink; + use ServiceLink; + + /** @var NotificationList */ + protected $list; + + protected function init(): void + { + $this->setNoSubjectLink($this->list->getNoSubjectLink()); + $this->list->addDetailFilterAttribute($this, Filter::equal('id', bin2hex($this->item->history->id))); + } + + /** + * Get a localized phrase for the given notification type + * + * @param string $type + * + * @return string + */ + public static function phraseForType(string $type): string + { + switch ($type) { + case 'acknowledgement': + return t('Problem acknowledged'); + case 'custom': + return t('Custom Notification triggered'); + case 'downtime_end': + return t('Downtime ended'); + case 'downtime_removed': + return t('Downtime removed'); + case 'downtime_start': + return t('Downtime started'); + case 'flapping_end': + return t('Flapping stopped'); + case 'flapping_start': + return t('Flapping started'); + case 'problem': + return t('%s ran into a problem'); + case 'recovery': + return t('%s recovered'); + default: + throw new InvalidArgumentException(sprintf('Type %s is not a valid notification type', $type)); + } + } + + abstract protected function getStateBallSize(); + + protected function assembleCaption(BaseHtmlElement $caption): void + { + if (in_array($this->item->type, ['flapping_end', 'flapping_start', 'problem', 'recovery'])) { + $commandName = $this->item->object_type === 'host' + ? $this->item->host->checkcommand_name + : $this->item->service->checkcommand_name; + if (isset($commandName)) { + if (empty($this->item->text)) { + $caption->addHtml(new EmptyState(t('Output unavailable.'))); + } else { + $caption->addHtml(new PluginOutputContainer( + (new PluginOutput($this->item->text)) + ->setCommandName($commandName) + )); + } + } else { + $caption->addHtml(new EmptyState(t('Waiting for Icinga DB to synchronize the config.'))); + } + } else { + $caption->add([ + new Icon(Icons::USER), + $this->item->author, + ': ', + $this->item->text + ]); + } + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + switch ($this->item->type) { + case 'acknowledgement': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IS_ACKNOWLEDGED) + )); + + break; + case 'custom': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::NOTIFICATION) + )); + + break; + case 'downtime_end': + case 'downtime_removed': + case 'downtime_start': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IN_DOWNTIME) + )); + + break; + case 'flapping_end': + case 'flapping_start': + $visual->addHtml(HtmlElement::create( + 'div', + ['class' => ['icon-ball', 'ball-size-' . $this->getStateBallSize()]], + new Icon(Icons::IS_FLAPPING) + )); + + break; + case 'problem': + case 'recovery': + if ($this->item->object_type === 'host') { + $state = HostStates::text($this->item->state); + $previousHardState = HostStates::text($this->item->previous_hard_state); + } else { + $state = ServiceStates::text($this->item->state); + $previousHardState = ServiceStates::text($this->item->previous_hard_state); + } + + $visual->addHtml(new StateChange($state, $previousHardState)); + + break; + } + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + if ($this->getNoSubjectLink()) { + $title->addHtml(HtmlElement::create( + 'span', + ['class' => 'subject'], + sprintf(self::phraseForType($this->item->type), ucfirst($this->item->object_type)) + )); + } else { + $title->addHtml(new Link( + sprintf(self::phraseForType($this->item->type), ucfirst($this->item->object_type)), + Links::event($this->item->history), + ['class' => 'subject'] + )); + } + + if ($this->item->object_type === 'host') { + $link = $this->createHostLink($this->item->host, true); + } else { + $link = $this->createServiceLink($this->item->service, $this->item->host, true); + } + + $title->addHtml(Text::create(' '), $link); + } + + protected function createTimestamp(): ?BaseHtmlElement + { + return new TimeAgo($this->item->send_time->getTimestamp()); + } +} diff --git a/library/Icingadb/Widget/ItemList/BaseServiceListItem.php b/library/Icingadb/Widget/ItemList/BaseServiceListItem.php new file mode 100644 index 0000000..fe4f014 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/BaseServiceListItem.php @@ -0,0 +1,70 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Html\Attributes; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\StateBall; + +/** + * Service item of a service list. Represents one database row. + * + * @property Service $item + * @property ServiceList $list + */ +abstract class BaseServiceListItem extends StateListItem +{ + use NoSubjectLink; + + protected function createSubject() + { + $service = $this->item->display_name; + $host = [ + new StateBall($this->item->host->state->getStateText(), StateBall::SIZE_MEDIUM), + ' ', + $this->item->host->display_name + ]; + + $host = new Link($host, Links::host($this->item->host), ['class' => 'subject']); + if ($this->getNoSubjectLink()) { + $service = new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($service)); + } else { + $service = new Link($service, Links::service($this->item, $this->item->host), ['class' => 'subject']); + } + + return [Html::sprintf(t('%s on %s', '<service> on <host>'), $service, $host)]; + } + + protected function init(): void + { + parent::init(); + + if ($this->list->getNoSubjectLink()) { + $this->setNoSubjectLink(); + } + + $this->list->addMultiselectFilterAttribute( + $this, + Filter::all( + Filter::equal('service.name', $this->item->name), + Filter::equal('host.name', $this->item->host->name) + ) + ); + $this->list->addDetailFilterAttribute( + $this, + Filter::all( + Filter::equal('name', $this->item->name), + Filter::equal('host.name', $this->item->host->name) + ) + ); + } +} diff --git a/library/Icingadb/Widget/ItemList/CommandTransportList.php b/library/Icingadb/Widget/ItemList/CommandTransportList.php new file mode 100644 index 0000000..61d771d --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommandTransportList.php @@ -0,0 +1,26 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\DetailActions; +use ipl\Web\Common\BaseOrderedItemList; +use ipl\Web\Url; + +class CommandTransportList extends BaseOrderedItemList +{ + use DetailActions; + + protected function init(): void + { + $this->getAttributes()->add('class', 'command-transport-list'); + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/command-transport/show')); + } + + protected function getItemClass(): string + { + return CommandTransportListItem::class; + } +} diff --git a/library/Icingadb/Widget/ItemList/CommandTransportListItem.php b/library/Icingadb/Widget/ItemList/CommandTransportListItem.php new file mode 100644 index 0000000..9873403 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommandTransportListItem.php @@ -0,0 +1,71 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseOrderedListItem; +use ipl\Web\Url; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; + +class CommandTransportListItem extends BaseOrderedListItem +{ + protected function init(): void + { + $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + } + + protected function assembleHeader(BaseHtmlElement $header): void + { + } + + protected function assembleMain(BaseHtmlElement $main): void + { + $main->addHtml(new Link( + new HtmlElement('strong', null, Text::create($this->item->name)), + Url::fromPath('icingadb/command-transport/show', ['name' => $this->item->name]) + )); + + $main->addHtml(new Link( + new Icon('trash', ['title' => sprintf(t('Remove command transport "%s"'), $this->item->name)]), + Url::fromPath('icingadb/command-transport/remove', ['name' => $this->item->name]), + [ + 'class' => 'pull-right action-link', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + + if ($this->getOrder() + 1 < $this->list->count()) { + $main->addHtml((new Link( + new Icon('arrow-down'), + Url::fromPath('icingadb/command-transport/sort', [ + 'name' => $this->item->name, + 'pos' => $this->getOrder() + 1 + ]), + ['class' => 'pull-right action-link'] + ))->setBaseTarget('_self')); + } + + if ($this->getOrder() > 0) { + $main->addHtml((new Link( + new Icon('arrow-up'), + Url::fromPath('icingadb/command-transport/sort', [ + 'name' => $this->item->name, + 'pos' => $this->getOrder() - 1 + ]), + ['class' => 'pull-right action-link'] + ))->setBaseTarget('_self')); + } + } + + protected function createVisual(): ?BaseHtmlElement + { + return null; + } +} diff --git a/library/Icingadb/Widget/ItemList/CommentList.php b/library/Icingadb/Widget/ItemList/CommentList.php new file mode 100644 index 0000000..5cf65ae --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommentList.php @@ -0,0 +1,49 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\CaptionDisabled; +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ObjectLinkDisabled; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Web\Common\BaseItemList; +use ipl\Web\Url; + +class CommentList extends BaseItemList +{ + use CaptionDisabled; + use NoSubjectLink; + use ObjectLinkDisabled; + use ViewMode; + use TicketLinks; + use DetailActions; + + protected $defaultAttributes = ['class' => 'comment-list']; + + protected function getItemClass(): string + { + $viewMode = $this->getViewMode(); + + $this->addAttributes(['class' => $viewMode]); + + if ($viewMode === 'minimal') { + return CommentListItemMinimal::class; + } elseif ($viewMode === 'detailed') { + $this->removeAttribute('class', 'default-layout'); + } + + return CommentListItem::class; + } + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::commentsDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/comment')); + } +} diff --git a/library/Icingadb/Widget/ItemList/CommentListItem.php b/library/Icingadb/Widget/ItemList/CommentListItem.php new file mode 100644 index 0000000..3bbd0c2 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommentListItem.php @@ -0,0 +1,12 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; + +class CommentListItem extends BaseCommentListItem +{ + use ListItemCommonLayout; +} diff --git a/library/Icingadb/Widget/ItemList/CommentListItemMinimal.php b/library/Icingadb/Widget/ItemList/CommentListItemMinimal.php new file mode 100644 index 0000000..3c23ccd --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommentListItemMinimal.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; + +class CommentListItemMinimal extends BaseCommentListItem +{ + use ListItemMinimalLayout; + + protected function init(): void + { + parent::init(); + + if ($this->list->isCaptionDisabled()) { + $this->setCaptionDisabled(); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/DowntimeList.php b/library/Icingadb/Widget/ItemList/DowntimeList.php new file mode 100644 index 0000000..591ad98 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/DowntimeList.php @@ -0,0 +1,49 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\CaptionDisabled; +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ObjectLinkDisabled; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Web\Common\BaseItemList; +use ipl\Web\Url; + +class DowntimeList extends BaseItemList +{ + use CaptionDisabled; + use NoSubjectLink; + use ObjectLinkDisabled; + use ViewMode; + use TicketLinks; + use DetailActions; + + protected $defaultAttributes = ['class' => 'downtime-list']; + + protected function getItemClass(): string + { + $viewMode = $this->getViewMode(); + + $this->addAttributes(['class' => $viewMode]); + + if ($viewMode === 'minimal') { + return DowntimeListItemMinimal::class; + } elseif ($viewMode === 'detailed') { + $this->removeAttribute('class', 'default-layout'); + } + + return DowntimeListItem::class; + } + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::downtimesDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/downtime')); + } +} diff --git a/library/Icingadb/Widget/ItemList/DowntimeListItem.php b/library/Icingadb/Widget/ItemList/DowntimeListItem.php new file mode 100644 index 0000000..cb7e9b3 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/DowntimeListItem.php @@ -0,0 +1,23 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; +use ipl\Html\BaseHtmlElement; + +class DowntimeListItem extends BaseDowntimeListItem +{ + use ListItemCommonLayout; + + protected function assembleMain(BaseHtmlElement $main): void + { + if ($this->item->is_in_effect) { + $main->add($this->createProgress()); + } + + $main->add($this->createHeader()); + $main->add($this->createCaption()); + } +} diff --git a/library/Icingadb/Widget/ItemList/DowntimeListItemMinimal.php b/library/Icingadb/Widget/ItemList/DowntimeListItemMinimal.php new file mode 100644 index 0000000..b8581d2 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/DowntimeListItemMinimal.php @@ -0,0 +1,21 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; + +class DowntimeListItemMinimal extends BaseDowntimeListItem +{ + use ListItemMinimalLayout; + + protected function init(): void + { + parent::init(); + + if ($this->list->isCaptionDisabled()) { + $this->setCaptionDisabled(); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/HistoryList.php b/library/Icingadb/Widget/ItemList/HistoryList.php new file mode 100644 index 0000000..d3b6232 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HistoryList.php @@ -0,0 +1,57 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\CaptionDisabled; +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\LoadMore; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\TicketLinks; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Orm\ResultSet; +use ipl\Web\Common\BaseItemList; +use ipl\Web\Url; + +class HistoryList extends BaseItemList +{ + use CaptionDisabled; + use NoSubjectLink; + use ViewMode; + use LoadMore; + use TicketLinks; + use DetailActions; + + protected $defaultAttributes = ['class' => 'history-list']; + + protected function init(): void + { + /** @var ResultSet $data */ + $data = $this->data; + $this->data = $this->getIterator($data); + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/event')); + } + + protected function getItemClass(): string + { + switch ($this->getViewMode()) { + case 'minimal': + return HistoryListItemMinimal::class; + case 'detailed': + $this->removeAttribute('class', 'default-layout'); + + return HistoryListItemDetailed::class; + default: + return HistoryListItem::class; + } + } + + protected function assemble(): void + { + $this->addAttributes(['class' => $this->getViewMode()]); + + parent::assemble(); + } +} diff --git a/library/Icingadb/Widget/ItemList/HistoryListItem.php b/library/Icingadb/Widget/ItemList/HistoryListItem.php new file mode 100644 index 0000000..c44a807 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HistoryListItem.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; +use ipl\Web\Widget\StateBall; + +class HistoryListItem extends BaseHistoryListItem +{ + use ListItemCommonLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/HistoryListItemDetailed.php b/library/Icingadb/Widget/ItemList/HistoryListItemDetailed.php new file mode 100644 index 0000000..7129d2d --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HistoryListItemDetailed.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemDetailedLayout; +use ipl\Web\Widget\StateBall; + +class HistoryListItemDetailed extends BaseHistoryListItem +{ + use ListItemDetailedLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/HistoryListItemMinimal.php b/library/Icingadb/Widget/ItemList/HistoryListItemMinimal.php new file mode 100644 index 0000000..5a7f214 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HistoryListItemMinimal.php @@ -0,0 +1,27 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; +use ipl\Web\Widget\StateBall; + +class HistoryListItemMinimal extends BaseHistoryListItem +{ + use ListItemMinimalLayout; + + protected function init(): void + { + parent::init(); + + if ($this->list->isCaptionDisabled()) { + $this->setCaptionDisabled(); + } + } + + protected function getStateBallSize(): string + { + return StateBall::SIZE_BIG; + } +} diff --git a/library/Icingadb/Widget/ItemList/HostDetailHeader.php b/library/Icingadb/Widget/ItemList/HostDetailHeader.php new file mode 100644 index 0000000..97176da --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HostDetailHeader.php @@ -0,0 +1,67 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Widget\StateChange; +use ipl\Html\BaseHtmlElement; +use ipl\Web\Widget\StateBall; + +class HostDetailHeader extends HostListItemMinimal +{ + protected function getStateBallSize(): string + { + return ''; + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + if ($this->state->state_type === 'soft') { + $stateType = 'soft_state'; + $previousStateType = 'previous_soft_state'; + + if ($this->state->previous_soft_state === 0) { + $previousStateType = 'hard_state'; + } + } else { + $stateType = 'hard_state'; + $previousStateType = 'previous_hard_state'; + + if ($this->state->hard_state === $this->state->previous_hard_state) { + $previousStateType = 'previous_soft_state'; + } + } + + $state = HostStates::text($this->state->$stateType); + $previousState = HostStates::text($this->state->$previousStateType); + + $stateChange = new StateChange($state, $previousState); + if ($stateType === 'soft_state') { + $stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + } + + if ($previousStateType === 'previous_soft_state') { + $stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + if ($stateType === 'soft_state') { + $visual->getAttributes()->add('class', 'small-state-change'); + } + } + + $stateChange->setIcon($this->state->getIcon()); + $stateChange->setHandled($this->state->is_handled || ! $this->state->is_reachable); + + $visual->addHtml($stateChange); + } + + protected function assemble(): void + { + $attributes = $this->list->getAttributes(); + if (! in_array('minimal', $attributes->get('class')->getValue())) { + $attributes->add('class', 'minimal'); + } + + parent::assemble(); + } +} diff --git a/library/Icingadb/Widget/ItemList/HostList.php b/library/Icingadb/Widget/ItemList/HostList.php new file mode 100644 index 0000000..2be1f84 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HostList.php @@ -0,0 +1,39 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\Links; +use ipl\Web\Url; + +/** + * Host list + */ +class HostList extends StateList +{ + protected $defaultAttributes = ['class' => 'host-list']; + + protected function getItemClass(): string + { + switch ($this->getViewMode()) { + case 'minimal': + return HostListItemMinimal::class; + case 'detailed': + $this->removeAttribute('class', 'default-layout'); + + return HostListItemDetailed::class; + case 'objectHeader': + return HostDetailHeader::class; + default: + return HostListItem::class; + } + } + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::hostsDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/host')); + } +} diff --git a/library/Icingadb/Widget/ItemList/HostListItem.php b/library/Icingadb/Widget/ItemList/HostListItem.php new file mode 100644 index 0000000..2eae660 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HostListItem.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; +use ipl\Web\Widget\StateBall; + +class HostListItem extends BaseHostListItem +{ + use ListItemCommonLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/HostListItemDetailed.php b/library/Icingadb/Widget/ItemList/HostListItemDetailed.php new file mode 100644 index 0000000..255bdcc --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HostListItemDetailed.php @@ -0,0 +1,108 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemDetailedLayout; +use Icinga\Module\Icingadb\Util\PerfDataSet; +use Icinga\Module\Icingadb\Widget\ItemList\CommentList; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; + +class HostListItemDetailed extends BaseHostListItem +{ + use ListItemDetailedLayout; + + /** @var int Max pie charts to be shown */ + const PIE_CHART_LIMIT = 5; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } + + protected function assembleFooter(BaseHtmlElement $footer): void + { + $statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons'])); + + if ($this->item->state->last_comment->host_id === $this->item->id) { + $comment = $this->item->state->last_comment; + $comment->host = $this->item; + $comment = (new CommentList([$comment])) + ->setNoSubjectLink() + ->setObjectLinkDisabled() + ->setDetailActionsDisabled(); + + $statusIcons->addHtml( + new HtmlElement( + 'div', + Attributes::create(['class' => 'comment-wrapper']), + new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment), + (new Icon('comments', ['class' => 'comment-icon'])) + ) + ); + } + + if ($this->item->state->is_flapping) { + $statusIcons->addHtml(new Icon( + 'random', + [ + 'title' => sprintf(t('Host "%s" is in flapping state'), $this->item->display_name), + ] + )); + } + + if (! $this->item->notifications_enabled) { + $statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')])); + } + + if (! $this->item->active_checks_enabled) { + $statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')])); + } + + $performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data'])); + if ($this->item->state->performance_data) { + $pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray(); + + $pies = []; + foreach ($pieChartData as $i => $perfdata) { + if ($perfdata->isVisualizable()) { + $pies[] = $perfdata->asInlinePie()->render(); + } + + // Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT + if (count($pies) > HostListItemDetailed::PIE_CHART_LIMIT) { + break; + } + } + + $maxVisiblePies = HostListItemDetailed::PIE_CHART_LIMIT - 2; + $numOfPies = count($pies); + foreach ($pies as $i => $pie) { + if ( + // Show max. 5 elements: if there are more than 5, show 4 + `…` + $i > $maxVisiblePies && $numOfPies > HostListItemDetailed::PIE_CHART_LIMIT + ) { + $performanceData->addHtml(new HtmlElement('span', null, Text::create('…'))); + break; + } + + $performanceData->addHtml(HtmlString::create($pie)); + } + } + + if (! $statusIcons->isEmpty()) { + $footer->addHtml($statusIcons); + } + + if (! $performanceData->isEmpty()) { + $footer->addHtml($performanceData); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/HostListItemMinimal.php b/library/Icingadb/Widget/ItemList/HostListItemMinimal.php new file mode 100644 index 0000000..f04b991 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/HostListItemMinimal.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; +use ipl\Web\Widget\StateBall; + +class HostListItemMinimal extends BaseHostListItem +{ + use ListItemMinimalLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_BIG; + } +} diff --git a/library/Icingadb/Widget/ItemList/NotificationList.php b/library/Icingadb/Widget/ItemList/NotificationList.php new file mode 100644 index 0000000..3a16b0b --- /dev/null +++ b/library/Icingadb/Widget/ItemList/NotificationList.php @@ -0,0 +1,55 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\CaptionDisabled; +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\LoadMore; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Orm\ResultSet; +use ipl\Web\Common\BaseItemList; +use ipl\Web\Url; + +class NotificationList extends BaseItemList +{ + use CaptionDisabled; + use NoSubjectLink; + use ViewMode; + use LoadMore; + use DetailActions; + + protected $defaultAttributes = ['class' => 'notification-list']; + + protected function init(): void + { + /** @var ResultSet $data */ + $data = $this->data; + $this->data = $this->getIterator($data); + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/event')); + } + + protected function getItemClass(): string + { + switch ($this->getViewMode()) { + case 'minimal': + return NotificationListItemMinimal::class; + case 'detailed': + $this->removeAttribute('class', 'default-layout'); + + return NotificationListItemDetailed::class; + default: + return NotificationListItem::class; + } + } + + protected function assemble(): void + { + $this->addAttributes(['class' => $this->getViewMode()]); + + parent::assemble(); + } +} diff --git a/library/Icingadb/Widget/ItemList/NotificationListItem.php b/library/Icingadb/Widget/ItemList/NotificationListItem.php new file mode 100644 index 0000000..683762f --- /dev/null +++ b/library/Icingadb/Widget/ItemList/NotificationListItem.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; +use ipl\Web\Widget\StateBall; + +class NotificationListItem extends BaseNotificationListItem +{ + use ListItemCommonLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/NotificationListItemDetailed.php b/library/Icingadb/Widget/ItemList/NotificationListItemDetailed.php new file mode 100644 index 0000000..0a7449e --- /dev/null +++ b/library/Icingadb/Widget/ItemList/NotificationListItemDetailed.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemDetailedLayout; +use ipl\Web\Widget\StateBall; + +class NotificationListItemDetailed extends BaseNotificationListItem +{ + use ListItemDetailedLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/NotificationListItemMinimal.php b/library/Icingadb/Widget/ItemList/NotificationListItemMinimal.php new file mode 100644 index 0000000..dd6d226 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/NotificationListItemMinimal.php @@ -0,0 +1,27 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; +use ipl\Web\Widget\StateBall; + +class NotificationListItemMinimal extends BaseNotificationListItem +{ + use ListItemMinimalLayout; + + protected function init(): void + { + parent::init(); + + if ($this->list->isCaptionDisabled()) { + $this->setCaptionDisabled(); + } + } + + protected function getStateBallSize(): string + { + return StateBall::SIZE_BIG; + } +} diff --git a/library/Icingadb/Widget/ItemList/PageSeparatorItem.php b/library/Icingadb/Widget/ItemList/PageSeparatorItem.php new file mode 100644 index 0000000..3e252eb --- /dev/null +++ b/library/Icingadb/Widget/ItemList/PageSeparatorItem.php @@ -0,0 +1,36 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class PageSeparatorItem extends BaseHtmlElement +{ + protected $defaultAttributes = ['class' => 'list-item page-separator']; + + /** @var int */ + protected $pageNumber; + + /** @var string */ + protected $tag = 'li'; + + public function __construct(int $pageNumber) + { + $this->pageNumber = $pageNumber; + } + + protected function assemble() + { + $this->add(Html::tag( + 'a', + [ + 'id' => 'page-' . $this->pageNumber, + 'data-icinga-no-scroll-on-focus' => true + ], + $this->pageNumber + )); + } +} diff --git a/library/Icingadb/Widget/ItemList/ServiceDetailHeader.php b/library/Icingadb/Widget/ItemList/ServiceDetailHeader.php new file mode 100644 index 0000000..2f0dbbd --- /dev/null +++ b/library/Icingadb/Widget/ItemList/ServiceDetailHeader.php @@ -0,0 +1,67 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Widget\StateChange; +use ipl\Html\BaseHtmlElement; +use ipl\Web\Widget\StateBall; + +class ServiceDetailHeader extends ServiceListItemMinimal +{ + protected function getStateBallSize(): string + { + return ''; + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + if ($this->state->state_type === 'soft') { + $stateType = 'soft_state'; + $previousStateType = 'previous_soft_state'; + + if ($this->state->previous_soft_state === 0) { + $previousStateType = 'hard_state'; + } + } else { + $stateType = 'hard_state'; + $previousStateType = 'previous_hard_state'; + + if ($this->state->hard_state === $this->state->previous_hard_state) { + $previousStateType = 'previous_soft_state'; + } + } + + $state = ServiceStates::text($this->state->$stateType); + $previousState = ServiceStates::text($this->state->$previousStateType); + + $stateChange = new StateChange($state, $previousState); + if ($stateType === 'soft_state') { + $stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + } + + if ($previousStateType === 'previous_soft_state') { + $stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE); + if ($stateType === 'soft_state') { + $visual->getAttributes()->add('class', 'small-state-change'); + } + } + + $stateChange->setIcon($this->state->getIcon()); + $stateChange->setHandled($this->state->is_handled || ! $this->state->is_reachable); + + $visual->addHtml($stateChange); + } + + protected function assemble(): void + { + $attributes = $this->list->getAttributes(); + if (! in_array('minimal', $attributes->get('class')->getValue())) { + $attributes->add('class', 'minimal'); + } + + parent::assemble(); + } +} diff --git a/library/Icingadb/Widget/ItemList/ServiceList.php b/library/Icingadb/Widget/ItemList/ServiceList.php new file mode 100644 index 0000000..8d41a70 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/ServiceList.php @@ -0,0 +1,36 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\Links; +use ipl\Web\Url; + +class ServiceList extends StateList +{ + protected $defaultAttributes = ['class' => 'service-list']; + + protected function getItemClass(): string + { + switch ($this->getViewMode()) { + case 'minimal': + return ServiceListItemMinimal::class; + case 'detailed': + $this->removeAttribute('class', 'default-layout'); + + return ServiceListItemDetailed::class; + case 'objectHeader': + return ServiceDetailHeader::class; + default: + return ServiceListItem::class; + } + } + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::servicesDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/service')); + } +} diff --git a/library/Icingadb/Widget/ItemList/ServiceListItem.php b/library/Icingadb/Widget/ItemList/ServiceListItem.php new file mode 100644 index 0000000..a974581 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/ServiceListItem.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemCommonLayout; +use ipl\Web\Widget\StateBall; + +class ServiceListItem extends BaseServiceListItem +{ + use ListItemCommonLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } +} diff --git a/library/Icingadb/Widget/ItemList/ServiceListItemDetailed.php b/library/Icingadb/Widget/ItemList/ServiceListItemDetailed.php new file mode 100644 index 0000000..1613599 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/ServiceListItemDetailed.php @@ -0,0 +1,112 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemDetailedLayout; +use Icinga\Module\Icingadb\Util\PerfDataSet; +use Icinga\Module\Icingadb\Widget\ItemList\CommentList; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; + +class ServiceListItemDetailed extends BaseServiceListItem +{ + use ListItemDetailedLayout; + + /** @var int Max pie charts to be shown */ + const PIE_CHART_LIMIT = 5; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_LARGE; + } + + protected function assembleFooter(BaseHtmlElement $footer): void + { + $statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons'])); + + if ($this->item->state->last_comment->service_id === $this->item->id) { + $comment = $this->item->state->last_comment; + $comment->service = $this->item; + $comment = (new CommentList([$comment])) + ->setNoSubjectLink() + ->setObjectLinkDisabled() + ->setDetailActionsDisabled(); + + $statusIcons->addHtml( + new HtmlElement( + 'div', + Attributes::create(['class' => 'comment-wrapper']), + new HtmlElement('div', Attributes::create(['class' => 'comment-popup']), $comment), + (new Icon('comments', ['class' => 'comment-icon'])) + ) + ); + } + + if ($this->item->state->is_flapping) { + $statusIcons->addHtml(new Icon( + 'random', + [ + 'title' => sprintf( + t('Service "%s" on "%s" is in flapping state'), + $this->item->display_name, + $this->item->host->display_name + ), + ] + )); + } + + if (! $this->item->notifications_enabled) { + $statusIcons->addHtml(new Icon('bell-slash', ['title' => t('Notifications disabled')])); + } + + if (! $this->item->active_checks_enabled) { + $statusIcons->addHtml(new Icon('eye-slash', ['title' => t('Active checks disabled')])); + } + + $performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data'])); + if ($this->item->state->performance_data) { + $pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray(); + + $pies = []; + foreach ($pieChartData as $i => $perfdata) { + if ($perfdata->isVisualizable()) { + $pies[] = $perfdata->asInlinePie()->render(); + } + + // Check if number of visualizable pie charts is larger than PIE_CHART_LIMIT + if (count($pies) > ServiceListItemDetailed::PIE_CHART_LIMIT) { + break; + } + } + + $maxVisiblePies = ServiceListItemDetailed::PIE_CHART_LIMIT - 2; + $numOfPies = count($pies); + foreach ($pies as $i => $pie) { + if ( + // Show max. 5 elements: if there are more than 5, show 4 + `…` + $i > $maxVisiblePies && $numOfPies > ServiceListItemDetailed::PIE_CHART_LIMIT + ) { + $performanceData->addHtml(new HtmlElement('span', null, Text::create('…'))); + break; + } + + $performanceData->addHtml(HtmlString::create($pie)); + } + } + + if (! $statusIcons->isEmpty()) { + $footer->addHtml($statusIcons); + } + + if (! $performanceData->isEmpty()) { + $footer->addHtml($performanceData); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/ServiceListItemMinimal.php b/library/Icingadb/Widget/ItemList/ServiceListItemMinimal.php new file mode 100644 index 0000000..e7a1bc6 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/ServiceListItemMinimal.php @@ -0,0 +1,18 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\ListItemMinimalLayout; +use ipl\Web\Widget\StateBall; + +class ServiceListItemMinimal extends BaseServiceListItem +{ + use ListItemMinimalLayout; + + protected function getStateBallSize(): string + { + return StateBall::SIZE_BIG; + } +} diff --git a/library/Icingadb/Widget/ItemList/StateList.php b/library/Icingadb/Widget/ItemList/StateList.php new file mode 100644 index 0000000..1e6dcb9 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/StateList.php @@ -0,0 +1,60 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\NoSubjectLink; +use Icinga\Module\Icingadb\Common\ViewMode; +use Icinga\Module\Icingadb\Redis\VolatileStateResults; +use Icinga\Module\Icingadb\Widget\Notice; +use ipl\Html\HtmlDocument; +use ipl\Web\Common\BaseItemList; + +abstract class StateList extends BaseItemList +{ + use ViewMode; + use NoSubjectLink; + use DetailActions; + + /** @var bool Whether the list contains at least one item with an icon_image */ + protected $hasIconImages = false; + + /** + * Get whether the list contains at least one item with an icon_image + * + * @return bool + */ + public function hasIconImages(): bool + { + return $this->hasIconImages; + } + + /** + * Set whether the list contains at least one item with an icon_image + * + * @param bool $hasIconImages + * + * @return $this + */ + public function setHasIconImages(bool $hasIconImages): self + { + $this->hasIconImages = $hasIconImages; + + return $this; + } + + protected function assemble(): void + { + $this->addAttributes(['class' => $this->getViewMode()]); + + parent::assemble(); + + if ($this->data instanceof VolatileStateResults && $this->data->isRedisUnavailable()) { + $this->prependWrapper((new HtmlDocument())->addHtml(new Notice( + t('Icinga Redis is currently unavailable. The shown information might be outdated.') + ))); + } + } +} diff --git a/library/Icingadb/Widget/ItemList/StateListItem.php b/library/Icingadb/Widget/ItemList/StateListItem.php new file mode 100644 index 0000000..d0b3363 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/StateListItem.php @@ -0,0 +1,140 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemList; + +use Icinga\Module\Icingadb\Common\Icons; +use Icinga\Module\Icingadb\Model\State; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\CheckAttempt; +use Icinga\Module\Icingadb\Widget\IconImage; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use ipl\Html\HtmlElement; +use ipl\Web\Common\BaseListItem; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\TimeSince; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; + +/** + * Host or service item of a host or service list. Represents one database row. + */ +abstract class StateListItem extends BaseListItem +{ + /** @var StateList The list where the item is part of */ + protected $list; + + /** @var State The state of the item */ + protected $state; + + protected function init(): void + { + $this->state = $this->item->state; + + if (isset($this->item->icon_image->icon_image)) { + $this->list->setHasIconImages(true); + } + } + + abstract protected function createSubject(); + + abstract protected function getStateBallSize(): string; + + /** + * @return ?BaseHtmlElement + */ + protected function createIconImage(): ?BaseHtmlElement + { + if (! $this->list->hasIconImages()) { + return null; + } + + $iconImage = HtmlElement::create('div', [ + 'class' => 'icon-image', + ]); + + $this->assembleIconImage($iconImage); + + return $iconImage; + } + + protected function assembleCaption(BaseHtmlElement $caption): void + { + if ($this->state->soft_state === null && $this->state->output === null) { + $caption->addHtml(Text::create(t('Waiting for Icinga DB to synchronize the state.'))); + } else { + if (empty($this->state->output)) { + $pluginOutput = new EmptyState(t('Output unavailable.')); + } else { + $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item)); + } + + $caption->addHtml($pluginOutput); + } + } + + protected function assembleIconImage(BaseHtmlElement $iconImage): void + { + if (isset($this->item->icon_image->icon_image)) { + $iconImage->addHtml(new IconImage($this->item->icon_image->icon_image, $this->item->icon_image_alt)); + } else { + $iconImage->addAttributes(['class' => 'placeholder']); + } + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml(Html::sprintf( + t('%s is %s', '<hostname> is <state-text>'), + $this->createSubject(), + Html::tag('span', ['class' => 'state-text'], $this->state->getStateTextTranslated()) + )); + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $stateBall = new StateBall($this->state->getStateText(), $this->getStateBallSize()); + $stateBall->add($this->state->getIcon()); + if ($this->state->is_handled || ! $this->state->is_reachable) { + $stateBall->getAttributes()->add('class', 'handled'); + } + + $visual->addHtml($stateBall); + if ($this->state->state_type === 'soft') { + $visual->addHtml( + new CheckAttempt((int) $this->state->check_attempt, (int) $this->item->max_check_attempts) + ); + } + } + + protected function createTimestamp(): ?BaseHtmlElement + { + $since = null; + if ($this->state->is_overdue) { + $since = new TimeSince($this->state->next_update->getTimestamp()); + $since->prepend(t('Overdue') . ' '); + $since->prependHtml(new Icon(Icons::WARNING)); + } elseif ($this->state->last_state_change !== null && $this->state->last_state_change->getTimestamp() > 0) { + $since = new TimeSince($this->state->last_state_change->getTimestamp()); + } + + return $since; + } + + protected function assemble(): void + { + if ($this->state->is_overdue) { + $this->addAttributes(['class' => 'overdue']); + } + + $this->add([ + $this->createVisual(), + $this->createIconImage(), + $this->createMain() + ]); + } +} diff --git a/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php b/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php new file mode 100644 index 0000000..c56a1f8 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/BaseHostGroupItem.php @@ -0,0 +1,60 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\Hostgroup; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseTableRowItem; +use ipl\Web\Widget\Link; + +/** + * Hostgroup item of a hostgroup list. Represents one database row. + * + * @property Hostgroup $item + * @property HostgroupTable $table + */ +abstract class BaseHostGroupItem extends BaseTableRowItem +{ + use Translation; + + protected function init(): void + { + if (isset($this->table)) { + $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + } + } + + protected function createSubject(): BaseHtmlElement + { + return isset($this->table) + ? new Link( + $this->item->display_name, + Links::hostgroup($this->item), + [ + 'class' => 'subject', + 'title' => sprintf( + $this->translate('List all hosts in the group "%s"'), + $this->item->display_name + ) + ] + ) + : new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ); + } + + protected function createCaption(): BaseHtmlElement + { + return new HtmlElement('span', null, Text::create($this->item->name)); + } +} diff --git a/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php b/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php new file mode 100644 index 0000000..7bee532 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/BaseServiceGroupItem.php @@ -0,0 +1,60 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\Servicegroup; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseTableRowItem; +use ipl\Web\Widget\Link; + +/** + * Servicegroup item of a servicegroup list. Represents one database row. + * + * @property Servicegroup $item + * @property ServicegroupTable $table + */ +abstract class BaseServiceGroupItem extends BaseTableRowItem +{ + use Translation; + + protected function init(): void + { + if (isset($this->table)) { + $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + } + } + + protected function createSubject(): BaseHtmlElement + { + return isset($this->table) + ? new Link( + $this->item->display_name, + Links::servicegroup($this->item), + [ + 'class' => 'subject', + 'title' => sprintf( + $this->translate('List all services in the group "%s"'), + $this->item->display_name + ) + ] + ) + : new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ); + } + + protected function createCaption(): BaseHtmlElement + { + return new HtmlElement('span', null, Text::create($this->item->name)); + } +} diff --git a/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php b/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php new file mode 100644 index 0000000..642d6b3 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php @@ -0,0 +1,107 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Orm\Model; + +/** @todo Figure out what this might (should) have in common with the new BaseTableRowItem implementation */ +abstract class BaseStateRowItem extends BaseHtmlElement +{ + protected $defaultAttributes = ['class' => 'row-item']; + + /** @var Model */ + protected $item; + + /** @var StateItemTable */ + protected $list; + + protected $tag = 'tr'; + + /** + * Create a new row item + * + * @param Model $item + * @param StateItemTable $list + */ + public function __construct(Model $item, StateItemTable $list) + { + $this->item = $item; + $this->list = $list; + + $this->init(); + } + + /** + * Initialize the row item + * + * If you want to adjust the row item after construction, override this method. + */ + protected function init() + { + } + + abstract protected function assembleVisual(BaseHtmlElement $visual); + + abstract protected function assembleCell(BaseHtmlElement $cell, string $path, $value); + + protected function createVisual(): BaseHtmlElement + { + $visual = new HtmlElement('td', Attributes::create(['class' => 'visual'])); + + $this->assembleVisual($visual); + + return $visual; + } + + protected function assemble() + { + $this->addHtml($this->createVisual()); + + foreach ($this->list->getColumns() as $columnPath => $_) { + $steps = explode('.', $columnPath); + if ($steps[0] === $this->item->getTableName()) { + array_shift($steps); + $columnPath = implode('.', $steps); + } + + $column = null; + $subject = $this->item; + foreach ($steps as $i => $step) { + if (isset($subject->$step)) { + if ($subject->$step instanceof Model) { + $subject = $subject->$step; + } else { + $column = $step; + } + } else { + $columnCandidate = implode('.', array_slice($steps, $i)); + if (isset($subject->$columnCandidate)) { + $column = $columnCandidate; + } else { + break; + } + } + } + + $value = null; + if ($column !== null) { + $value = $subject->$column; + if (is_array($value)) { + $value = empty($value) ? null : implode(',', $value); + } + } + + $cell = new HtmlElement('td'); + if ($value !== null) { + $this->assembleCell($cell, $columnPath, $value); + } + + $this->addHtml($cell); + } + } +} diff --git a/library/Icingadb/Widget/ItemTable/GridCellLayout.php b/library/Icingadb/Widget/ItemTable/GridCellLayout.php new file mode 100644 index 0000000..95b1a0a --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/GridCellLayout.php @@ -0,0 +1,39 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Html\BaseHtmlElement; +use ipl\Web\Widget\Link; + +trait GridCellLayout +{ + /** + * Creates a state badge for the Host / Service group with the highest severity that an object in the group has, + * along with the count of the objects with this severity belonging to the corresponding group. + * + * @return Link + */ + abstract public function createGroupBadge(): Link; + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $visual->add($this->createGroupBadge()); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml( + $this->createSubject(), + $this->createCaption() + ); + } + + protected function assemble(): void + { + $this->add([ + $this->createTitle() + ]); + } +} diff --git a/library/Icingadb/Widget/ItemTable/HostItemTable.php b/library/Icingadb/Widget/ItemTable/HostItemTable.php new file mode 100644 index 0000000..e303746 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/HostItemTable.php @@ -0,0 +1,31 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\Links; +use ipl\Web\Url; + +class HostItemTable extends StateItemTable +{ + use DetailActions; + + protected function init() + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::hostsDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/host')); + } + + protected function getItemClass(): string + { + return HostRowItem::class; + } + + protected function getVisualColumn(): string + { + return 'host.state.severity'; + } +} diff --git a/library/Icingadb/Widget/ItemTable/HostRowItem.php b/library/Icingadb/Widget/ItemTable/HostRowItem.php new file mode 100644 index 0000000..cff70dd --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/HostRowItem.php @@ -0,0 +1,51 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Html\BaseHtmlElement; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Link; + +class HostRowItem extends StateRowItem +{ + /** @var HostItemTable */ + protected $list; + + /** @var Host */ + protected $item; + + protected function init() + { + parent::init(); + + $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)) + ->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name)); + } + + protected function assembleCell(BaseHtmlElement $cell, string $path, $value) + { + switch ($path) { + case 'name': + case 'display_name': + $cell->addHtml(new Link($this->item->$path, Links::host($this->item), [ + 'class' => 'subject', + 'title' => $this->item->$path + ])); + break; + case 'service.name': + case 'service.display_name': + $column = substr($path, 8); + $cell->addHtml(new Link( + $this->item->service->$column, + Links::service($this->item->service, $this->item) + )); + break; + default: + parent::assembleCell($cell, $path, $value); + } + } +} diff --git a/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php b/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php new file mode 100644 index 0000000..5396747 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/HostgroupGridCell.php @@ -0,0 +1,114 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Stdlib\Filter; +use ipl\Web\Url; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\StateBadge; + +class HostgroupGridCell extends BaseHostGroupItem +{ + use GridCellLayout; + + protected $defaultAttributes = ['class' => ['group-grid-cell', 'hostgroup-grid-cell']]; + + protected function createGroupBadge(): Link + { + $url = Url::fromPath('icingadb/hosts'); + $urlFilter = Filter::all(Filter::equal('hostgroup.name', $this->item->name)); + + if ($this->item->hosts_down_unhandled > 0) { + $urlFilter->add(Filter::equal('host.state.soft_state', 1)) + ->add(Filter::equal('host.state.is_handled', 'n')) + ->add(Filter::equal('host.state.is_reachable', 'y')); + + return new Link( + new StateBadge($this->item->hosts_down_unhandled, 'down'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d host that is currently in DOWN state in host group "%s"', + 'List %d hosts which are currently in DOWN state in host group "%s"', + $this->item->hosts_down_unhandled + ), + $this->item->hosts_down_unhandled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->hosts_down_handled > 0) { + $urlFilter->add(Filter::equal('host.state.soft_state', 1)) + ->add(Filter::any( + Filter::equal('host.state.is_handled', 'y'), + Filter::equal('host.state.is_reachable', 'n') + )); + + return new Link( + new StateBadge($this->item->hosts_down_handled, 'down', true), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d host that is currently in DOWN (Acknowledged) state in host group "%s"', + 'List %d hosts which are currently in DOWN (Acknowledged) state in host group "%s"', + $this->item->hosts_down_handled + ), + $this->item->hosts_down_handled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->hosts_pending > 0) { + $urlFilter->add(Filter::equal('host.state.soft_state', 99)); + + return new Link( + new StateBadge($this->item->hosts_pending, 'pending'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d host that is currently in PENDING state in host group "%s"', + 'List %d hosts which are currently in PENDING state in host group "%s"', + $this->item->hosts_pending + ), + $this->item->hosts_pending, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->hosts_up > 0) { + $urlFilter->add(Filter::equal('host.state.soft_state', 0)); + + return new Link( + new StateBadge($this->item->hosts_up, 'up'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d host that is currently in UP state in host group "%s"', + 'List %d hosts which are currently in UP state in host group "%s"', + $this->item->hosts_up + ), + $this->item->hosts_up, + $this->item->display_name + ) + ] + ); + } + + return new Link( + new StateBadge(0, 'none'), + $url, + [ + 'title' => sprintf( + $this->translate('There are no hosts in host group "%s"'), + $this->item->display_name + ) + ] + ); + } +} diff --git a/library/Icingadb/Widget/ItemTable/HostgroupTable.php b/library/Icingadb/Widget/ItemTable/HostgroupTable.php new file mode 100644 index 0000000..6b40f76 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/HostgroupTable.php @@ -0,0 +1,38 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Web\Common\BaseItemTable; +use ipl\Web\Url; + +class HostgroupTable extends BaseItemTable +{ + use DetailActions; + use ViewMode; + + protected $defaultAttributes = ['class' => 'hostgroup-table']; + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/hostgroup')); + } + + protected function getLayout(): string + { + return $this->getViewMode() === 'grid' + ? 'group-grid' + : parent::getLayout(); + } + + protected function getItemClass(): string + { + return $this->getViewMode() === 'grid' + ? HostgroupGridCell::class + : HostgroupTableRow::class; + } +} diff --git a/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php b/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php new file mode 100644 index 0000000..6aa61c2 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/HostgroupTableRow.php @@ -0,0 +1,55 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Model\Hostgroup; +use Icinga\Module\Icingadb\Widget\Detail\HostStatistics; +use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics; +use ipl\Html\BaseHtmlElement; +use ipl\Stdlib\Filter; + +/** + * Hostgroup table row of a hostgroup table. Represents one database row. + * + * @property Hostgroup $item + * @property HostgroupTable $table + */ +class HostgroupTableRow extends BaseHostGroupItem +{ + use TableRowLayout; + + protected $defaultAttributes = ['class' => 'hostgroup-table-row']; + + /** + * Create Host and service statistics columns + * + * @return BaseHtmlElement[] + */ + protected function createStatistics(): array + { + $hostStats = new HostStatistics($this->item); + + $hostStats->setBaseFilter(Filter::equal('hostgroup.name', $this->item->name)); + if (isset($this->table) && $this->table->hasBaseFilter()) { + $hostStats->setBaseFilter( + Filter::all($hostStats->getBaseFilter(), $this->table->getBaseFilter()) + ); + } + + $serviceStats = new ServiceStatistics($this->item); + + $serviceStats->setBaseFilter(Filter::equal('hostgroup.name', $this->item->name)); + if (isset($this->table) && $this->table->hasBaseFilter()) { + $serviceStats->setBaseFilter( + Filter::all($serviceStats->getBaseFilter(), $this->table->getBaseFilter()) + ); + } + + return [ + $this->createColumn($hostStats), + $this->createColumn($serviceStats) + ]; + } +} diff --git a/library/Icingadb/Widget/ItemTable/ServiceItemTable.php b/library/Icingadb/Widget/ItemTable/ServiceItemTable.php new file mode 100644 index 0000000..60872d8 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/ServiceItemTable.php @@ -0,0 +1,31 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\Links; +use ipl\Web\Url; + +class ServiceItemTable extends StateItemTable +{ + use DetailActions; + + protected function init() + { + $this->initializeDetailActions(); + $this->setMultiselectUrl(Links::servicesDetails()); + $this->setDetailUrl(Url::fromPath('icingadb/service')); + } + + protected function getItemClass(): string + { + return ServiceRowItem::class; + } + + protected function getVisualColumn(): string + { + return 'service.state.severity'; + } +} diff --git a/library/Icingadb/Widget/ItemTable/ServiceRowItem.php b/library/Icingadb/Widget/ItemTable/ServiceRowItem.php new file mode 100644 index 0000000..0fb95d0 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/ServiceRowItem.php @@ -0,0 +1,64 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Html\BaseHtmlElement; +use ipl\Stdlib\Filter; +use ipl\Web\Widget\Link; + +class ServiceRowItem extends StateRowItem +{ + /** @var ServiceItemTable */ + protected $list; + + /** @var Service */ + protected $item; + + protected function init() + { + parent::init(); + + $this->list->addMultiselectFilterAttribute( + $this, + Filter::all( + Filter::equal('service.name', $this->item->name), + Filter::equal('host.name', $this->item->host->name) + ) + ); + $this->list->addDetailFilterAttribute( + $this, + Filter::all( + Filter::equal('name', $this->item->name), + Filter::equal('host.name', $this->item->host->name) + ) + ); + } + + protected function assembleCell(BaseHtmlElement $cell, string $path, $value) + { + switch ($path) { + case 'name': + case 'display_name': + $cell->addHtml(new Link( + $this->item->$path, + Links::service($this->item, $this->item->host), + [ + 'class' => 'subject', + 'title' => $this->item->$path + ] + )); + break; + case 'host.name': + case 'host.display_name': + $column = substr($path, 5); + $cell->addHtml(new Link($this->item->host->$column, Links::host($this->item->host))); + break; + default: + parent::assembleCell($cell, $path, $value); + } + } +} diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php b/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php new file mode 100644 index 0000000..16e50e1 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/ServicegroupGridCell.php @@ -0,0 +1,204 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Stdlib\Filter; +use ipl\Web\Url; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\StateBadge; + +class ServicegroupGridCell extends BaseServiceGroupItem +{ + use GridCellLayout; + + protected $defaultAttributes = ['class' => ['group-grid-cell', 'servicegroup-grid-cell']]; + + protected function createGroupBadge(): Link + { + $url = Url::fromPath('icingadb/services/grid'); + $urlFilter = Filter::all(Filter::equal('servicegroup.name', $this->item->name)); + + if ($this->item->services_critical_unhandled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 2)) + ->add(Filter::equal('service.state.is_handled', 'n')) + ->add(Filter::equal('service.state.is_reachable', 'y')); + + return new Link( + new StateBadge($this->item->services_critical_unhandled, 'critical'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in CRITICAL state in service group "%s"', + 'List %d services which are currently in CRITICAL state in service group "%s"', + $this->item->services_critical_unhandled + ), + $this->item->services_critical_unhandled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_critical_handled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 2)) + ->add(Filter::any( + Filter::equal('service.state.is_handled', 'y'), + Filter::equal('service.state.is_reachable', 'n') + )); + + return new Link( + new StateBadge($this->item->services_critical_handled, 'critical', true), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in CRITICAL (Acknowledged) state in service group' + . ' "%s"', + 'List %d services which are currently in CRITICAL (Acknowledged) state in service group' + . ' "%s"', + $this->item->services_critical_handled + ), + $this->item->services_critical_handled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_warning_unhandled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 1)) + ->add(Filter::equal('service.state.is_handled', 'n')) + ->add(Filter::equal('service.state.is_reachable', 'y')); + + return new Link( + new StateBadge($this->item->services_warning_unhandled, 'warning'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in WARNING state in service group "%s"', + 'List %d services which are currently in WARNING state in service group "%s"', + $this->item->services_warning_unhandled + ), + $this->item->services_warning_unhandled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_warning_handled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 1)) + ->add(Filter::any( + Filter::equal('service.state.is_handled', 'y'), + Filter::equal('service.state.is_reachable', 'n') + )); + + return new Link( + new StateBadge($this->item->services_warning_handled, 'warning', true), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in WARNING (Acknowledged) state in service group' + . ' "%s"', + 'List %d services which are currently in WARNING (Acknowledged) state in service group' + . ' "%s"', + $this->item->services_warning_handled + ), + $this->item->services_warning_handled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_unknown_unhandled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 3)) + ->add(Filter::equal('service.state.is_handled', 'n')) + ->add(Filter::equal('service.state.is_reachable', 'y')); + + return new Link( + new StateBadge($this->item->services_unknown_unhandled, 'unknown'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in UNKNOWN state in service group "%s"', + 'List %d services which are currently in UNKNOWN state in service group "%s"', + $this->item->services_unknown_unhandled + ), + $this->item->services_unknown_unhandled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_unknown_handled > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 3)) + ->add(Filter::any( + Filter::equal('service.state.is_handled', 'y'), + Filter::equal('service.state.is_reachable', 'n') + )); + + return new Link( + new StateBadge($this->item->services_unknown_handled, 'unknown', true), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in UNKNOWN (Acknowledged) state in service group' + . ' "%s"', + 'List %d services which are currently in UNKNOWN (Acknowledged) state in service group' + . ' "%s"', + $this->item->services_unknown_handled + ), + $this->item->services_unknown_handled, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_pending > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 99)); + + return new Link( + new StateBadge($this->item->services_pending, 'pending'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in PENDING state in service group "%s"', + 'List %d services which are currently in PENDING state in service group "%s"', + $this->item->services_pending + ), + $this->item->services_pending, + $this->item->display_name + ) + ] + ); + } elseif ($this->item->services_ok > 0) { + $urlFilter->add(Filter::equal('service.state.soft_state', 0)); + + return new Link( + new StateBadge($this->item->services_ok, 'ok'), + $url->setFilter($urlFilter), + [ + 'title' => sprintf( + $this->translatePlural( + 'List %d service that is currently in OK state in service group "%s"', + 'List %d services which are currently in OK state in service group "%s"', + $this->item->services_ok + ), + $this->item->services_ok, + $this->item->display_name + ) + ] + ); + } + + return new Link( + new StateBadge(0, 'none'), + $url, + [ + 'title' => sprintf( + $this->translate('There are no services in service group "%s"'), + $this->item->display_name + ) + ] + ); + } +} diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupTable.php b/library/Icingadb/Widget/ItemTable/ServicegroupTable.php new file mode 100644 index 0000000..2378a77 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/ServicegroupTable.php @@ -0,0 +1,38 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use Icinga\Module\Icingadb\Common\ViewMode; +use ipl\Web\Common\BaseItemTable; +use ipl\Web\Url; + +class ServicegroupTable extends BaseItemTable +{ + use DetailActions; + use ViewMode; + + protected $defaultAttributes = ['class' => 'servicegroup-table']; + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/servicegroup')); + } + + protected function getLayout(): string + { + return $this->getViewMode() === 'grid' + ? 'group-grid' + : parent::getLayout(); + } + + protected function getItemClass(): string + { + return $this->getViewMode() === 'grid' + ? ServicegroupGridCell::class + : ServicegroupTableRow::class; + } +} diff --git a/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php b/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php new file mode 100644 index 0000000..3dea4c1 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/ServicegroupTableRow.php @@ -0,0 +1,42 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Model\Servicegroup; +use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics; +use ipl\Html\BaseHtmlElement; +use ipl\Stdlib\Filter; + +/** + * Servicegroup item of a servicegroup list. Represents one database row. + * + * @property Servicegroup $item + * @property ServicegroupTable $table + */ +class ServicegroupTableRow extends BaseServiceGroupItem +{ + use TableRowLayout; + + protected $defaultAttributes = ['class' => 'servicegroup-table-row']; + + /** + * Create Service statistics cell + * + * @return BaseHtmlElement[] + */ + protected function createStatistics(): array + { + $serviceStats = new ServiceStatistics($this->item); + + $serviceStats->setBaseFilter(Filter::equal('servicegroup.name', $this->item->name)); + if (isset($this->table) && $this->table->hasBaseFilter()) { + $serviceStats->setBaseFilter( + Filter::all($serviceStats->getBaseFilter(), $this->table->getBaseFilter()) + ); + } + + return [$this->createColumn($serviceStats)]; + } +} diff --git a/library/Icingadb/Widget/ItemTable/StateItemTable.php b/library/Icingadb/Widget/ItemTable/StateItemTable.php new file mode 100644 index 0000000..f392322 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/StateItemTable.php @@ -0,0 +1,216 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Form; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Orm\Common\SortUtil; +use ipl\Orm\Query; +use ipl\Web\Control\SortControl; +use ipl\Web\Widget\EmptyStateBar; +use ipl\Web\Widget\Icon; + +/** @todo Figure out what this might (should) have in common with the new BaseItemTable implementation */ +abstract class StateItemTable extends BaseHtmlElement +{ + protected $baseAttributes = [ + 'class' => 'state-item-table' + ]; + + /** @var array<string, string> The columns to render */ + protected $columns; + + /** @var iterable The datasource */ + protected $data; + + /** @var string The sort rules */ + protected $sort; + + protected $tag = 'table'; + + /** + * Create a new item table + * + * @param iterable $data Datasource of the table + * @param array<string, string> $columns The columns to render, keys are labels + */ + public function __construct(iterable $data, array $columns) + { + $this->data = $data; + $this->columns = array_flip($columns); + + $this->addAttributes($this->baseAttributes); + + $this->init(); + } + + /** + * Initialize the item table + * + * If you want to adjust the item table after construction, override this method. + */ + protected function init() + { + } + + /** + * Get the columns being rendered + * + * @return array<string, string> + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * Set sort rules (as returned by {@see SortControl::getSort()}) + * + * @param ?string $sort + * + * @return $this + */ + public function setSort(?string $sort): self + { + $this->sort = $sort; + + return $this; + } + + abstract protected function getItemClass(): string; + + abstract protected function getVisualColumn(): string; + + protected function getVisualLabel() + { + return new Icon('heartbeat', ['title' => t('Severity')]); + } + + protected function assembleColumnHeader(BaseHtmlElement $header, string $name, $label): void + { + $sortRules = []; + if ($this->sort !== null) { + $sortRules = SortUtil::createOrderBy($this->sort); + } + + $active = false; + $sortDirection = null; + foreach ($sortRules as $rule) { + if ($rule[0] === $name) { + $sortDirection = $rule[1]; + $active = true; + break; + } + } + + if ($sortDirection === 'desc') { + $value = "$name asc"; + } else { + $value = "$name desc"; + } + + $icon = 'sort'; + if ($active) { + $icon = $sortDirection === 'desc' ? 'sort-up' : 'sort-down'; + } + + $form = new Form(); + $form->setAttribute('method', 'GET'); + + $button = $form->createElement('button', 'sort', [ + 'value' => $value, + 'type' => 'submit', + 'title' => is_string($label) ? $label : null, + 'class' => $active ? 'active' : null + ]); + $button->addHtml( + Html::tag( + 'span', + null, + // With to have the height sized the same as the others + $label ?? HtmlString::create(' ') + ), + new Icon($icon) + ); + $form->addElement($button); + + $header->add($form); + + switch (true) { + case substr($name, -7) === '.output': + case substr($name, -12) === '.long_output': + $header->getAttributes()->add('class', 'has-plugin-output'); + break; + case substr($name, -22) === '.icon_image.icon_image': + $header->getAttributes()->add('class', 'has-icon-images'); + break; + case substr($name, -17) === '.performance_data': + case substr($name, -28) === '.normalized_performance_data': + $header->getAttributes()->add('class', 'has-performance-data'); + break; + } + } + + protected function assemble() + { + $itemClass = $this->getItemClass(); + + $headerRow = new HtmlElement('tr'); + + $visualCell = new HtmlElement('th', Attributes::create(['class' => 'has-visual'])); + $this->assembleColumnHeader($visualCell, $this->getVisualColumn(), $this->getVisualLabel()); + $headerRow->addHtml($visualCell); + + foreach ($this->columns as $name => $label) { + $headerCell = new HtmlElement('th'); + $this->assembleColumnHeader($headerCell, $name, is_int($label) ? $name : $label); + $headerRow->addHtml($headerCell); + } + + $this->addHtml(new HtmlElement('thead', null, $headerRow)); + + $body = new HtmlElement('tbody', Attributes::create(['data-base-target' => '_next'])); + foreach ($this->data as $item) { + $body->addHtml(new $itemClass($item, $this)); + } + + if ($body->isEmpty()) { + $body->addHtml(new HtmlElement( + 'tr', + null, + new HtmlElement( + 'td', + Attributes::create(['colspan' => count($this->columns)]), + new EmptyStateBar(t('No items found.')) + ) + )); + } + + $this->addHtml($body); + } + + /** + * Enrich the given list of column names with appropriate labels + * + * @param Query $query + * @param array $columns + * + * @return array + */ + public static function applyColumnMetaData(Query $query, array $columns): array + { + $newColumns = []; + foreach ($columns as $columnPath) { + $label = $query->getResolver()->getColumnDefinition($columnPath)->getLabel(); + $newColumns[$label ?? $columnPath] = $columnPath; + } + + return $newColumns; + } +} diff --git a/library/Icingadb/Widget/ItemTable/StateRowItem.php b/library/Icingadb/Widget/ItemTable/StateRowItem.php new file mode 100644 index 0000000..f62286b --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/StateRowItem.php @@ -0,0 +1,124 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\HostStates; +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Util\PerfDataSet; +use Icinga\Module\Icingadb\Util\PluginOutput; +use Icinga\Module\Icingadb\Widget\CheckAttempt; +use Icinga\Module\Icingadb\Widget\IconImage; +use Icinga\Module\Icingadb\Widget\PluginOutputContainer; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Web\Widget\EmptyState; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; +use ipl\Web\Widget\TimeSince; +use ipl\Web\Widget\TimeUntil; + +abstract class StateRowItem extends BaseStateRowItem +{ + /** @var StateItemTable */ + protected $list; + + protected function assembleVisual(BaseHtmlElement $visual) + { + $stateBall = new StateBall($this->item->state->getStateText(), StateBall::SIZE_LARGE); + $stateBall->add($this->item->state->getIcon()); + + if ($this->item->state->is_handled) { + $stateBall->getAttributes()->add('class', 'handled'); + } + + $visual->addHtml($stateBall); + if ($this->item->state->state_type === 'soft') { + $visual->addHtml(new CheckAttempt( + (int) $this->item->state->check_attempt, + (int) $this->item->max_check_attempts + )); + } + } + + protected function assembleCell(BaseHtmlElement $cell, string $path, $value) + { + switch (true) { + case $path === 'state.output': + case $path === 'state.long_output': + if (empty($value)) { + $pluginOutput = new EmptyState(t('Output unavailable.')); + } else { + $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item)); + } + + $cell->addHtml($pluginOutput) + ->getAttributes() + ->add('class', 'has-plugin-output'); + break; + case $path === 'state.soft_state': + case $path === 'state.hard_state': + case $path === 'state.previous_soft_state': + case $path === 'state.previous_hard_state': + $stateType = substr($path, 6); + if ($this->item instanceof Host) { + $stateName = HostStates::translated($this->item->state->$stateType); + } else { + $stateName = ServiceStates::translated($this->item->state->$stateType); + } + + $cell->addHtml(Text::create($stateName)); + break; + case $path === 'state.last_update': + case $path === 'state.last_state_change': + $column = substr($path, 6); + $cell->addHtml(new TimeSince($this->item->state->$column->getTimestamp())); + break; + case $path === 'state.next_check': + case $path === 'state.next_update': + $column = substr($path, 6); + $cell->addHtml(new TimeUntil($this->item->state->$column->getTimestamp())); + break; + case $path === 'state.performance_data': + case $path === 'state.normalized_performance_data': + $perfdataContainer = new HtmlElement('div', Attributes::create(['class' => 'performance-data'])); + + $pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray(); + foreach ($pieChartData as $perfdata) { + if ($perfdata->isVisualizable()) { + $perfdataContainer->addHtml(new HtmlString($perfdata->asInlinePie()->render())); + } + } + + $cell->addHtml($perfdataContainer) + ->getAttributes() + ->add('class', 'has-performance-data'); + break; + case $path === 'is_volatile': + case $path === 'host.is_volatile': + case substr($path, -8) == '_enabled': + case (bool) preg_match('/state\.(is_|in_)/', $path): + if ($value) { + $cell->addHtml(new Icon('check')); + } + + break; + case $path === 'icon_image.icon_image': + $cell->addHtml(new IconImage($value, $this->item->icon_image_alt)) + ->getAttributes() + ->add('class', 'has-icon-images'); + break; + default: + if (preg_match('/(^id|_id|.id|_checksum|_bin)$/', $path)) { + $value = bin2hex($value); + } + + $cell->addHtml(Text::create($value)); + } + } +} diff --git a/library/Icingadb/Widget/ItemTable/TableRowLayout.php b/library/Icingadb/Widget/ItemTable/TableRowLayout.php new file mode 100644 index 0000000..b9ce022 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/TableRowLayout.php @@ -0,0 +1,26 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; + +trait TableRowLayout +{ + protected function assembleColumns(HtmlDocument $columns): void + { + foreach ($this->createStatistics() as $objectStatistic) { + $columns->addHtml($objectStatistic); + } + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml( + $this->createSubject(), + $this->createCaption() + ); + } +} diff --git a/library/Icingadb/Widget/ItemTable/UserTable.php b/library/Icingadb/Widget/ItemTable/UserTable.php new file mode 100644 index 0000000..432817b --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/UserTable.php @@ -0,0 +1,27 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use ipl\Web\Common\BaseItemTable; +use ipl\Web\Url; + +class UserTable extends BaseItemTable +{ + use DetailActions; + + protected $defaultAttributes = ['class' => 'user-table']; + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/user')); + } + + protected function getItemClass(): string + { + return UserTableRow::class; + } +} diff --git a/library/Icingadb/Widget/ItemTable/UserTableRow.php b/library/Icingadb/Widget/ItemTable/UserTableRow.php new file mode 100644 index 0000000..c10851e --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/UserTableRow.php @@ -0,0 +1,61 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\User; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseTableRowItem; +use ipl\Web\Widget\Link; + +/** + * User item of a user list. Represents one database row. + * + * @property User $item + * @property UserTable $table + */ +class UserTableRow extends BaseTableRowItem +{ + protected $defaultAttributes = ['class' => 'user-table-row']; + + protected function init(): void + { + if (isset($this->table)) { + $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + } + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $visual->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'user-ball']), + Text::create($this->item->display_name[0]) + )); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml( + isset($this->table) + ? new Link($this->item->display_name, Links::user($this->item), ['class' => 'subject']) + : new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ), + new HtmlElement('span', null, Text::create($this->item->name)) + ); + } + + protected function assembleColumns(HtmlDocument $columns): void + { + } +} diff --git a/library/Icingadb/Widget/ItemTable/UsergroupTable.php b/library/Icingadb/Widget/ItemTable/UsergroupTable.php new file mode 100644 index 0000000..77d3ba9 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/UsergroupTable.php @@ -0,0 +1,27 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\DetailActions; +use ipl\Web\Common\BaseItemTable; +use ipl\Web\Url; + +class UsergroupTable extends BaseItemTable +{ + use DetailActions; + + protected $defaultAttributes = ['class' => 'usergroup-table']; + + protected function init(): void + { + $this->initializeDetailActions(); + $this->setDetailUrl(Url::fromPath('icingadb/usergroup')); + } + + protected function getItemClass(): string + { + return UsergroupTableRow::class; + } +} diff --git a/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php b/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php new file mode 100644 index 0000000..c3cbf74 --- /dev/null +++ b/library/Icingadb/Widget/ItemTable/UsergroupTableRow.php @@ -0,0 +1,61 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget\ItemTable; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\Usergroup; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Stdlib\Filter; +use ipl\Web\Common\BaseTableRowItem; +use ipl\Web\Widget\Link; + +/** + * Usergroup item of a usergroup list. Represents one database row. + * + * @property Usergroup $item + * @property UsergroupTable $table + */ +class UsergroupTableRow extends BaseTableRowItem +{ + protected $defaultAttributes = ['class' => 'usergroup-table-row']; + + protected function init(): void + { + if (isset($this->table)) { + $this->table->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); + } + } + + protected function assembleVisual(BaseHtmlElement $visual): void + { + $visual->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'usergroup-ball']), + Text::create($this->item->display_name[0]) + )); + } + + protected function assembleTitle(BaseHtmlElement $title): void + { + $title->addHtml( + isset($this->table) + ? new Link($this->item->display_name, Links::usergroup($this->item), ['class' => 'subject']) + : new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($this->item->display_name) + ), + new HtmlElement('span', null, Text::create($this->item->name)) + ); + } + + protected function assembleColumns(HtmlDocument $columns): void + { + } +} diff --git a/library/Icingadb/Widget/MarkdownLine.php b/library/Icingadb/Widget/MarkdownLine.php new file mode 100644 index 0000000..74c413d --- /dev/null +++ b/library/Icingadb/Widget/MarkdownLine.php @@ -0,0 +1,28 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Web\Helper\Markdown; +use ipl\Html\BaseHtmlElement; +use ipl\Html\DeferredText; + +class MarkdownLine extends BaseHtmlElement +{ + protected $tag = 'section'; + + protected $defaultAttributes = ['class' => ['markdown', 'inline']]; + + /** + * MarkdownLine constructor. + * + * @param string $line + */ + public function __construct(string $line) + { + $this->add((new DeferredText(function () use ($line) { + return Markdown::line($line); + }))->setEscaped(true)); + } +} diff --git a/library/Icingadb/Widget/MarkdownText.php b/library/Icingadb/Widget/MarkdownText.php new file mode 100644 index 0000000..43db03e --- /dev/null +++ b/library/Icingadb/Widget/MarkdownText.php @@ -0,0 +1,28 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Web\Helper\Markdown; +use ipl\Html\BaseHtmlElement; +use ipl\Html\DeferredText; + +class MarkdownText extends BaseHtmlElement +{ + protected $tag = 'section'; + + protected $defaultAttributes = ['class' => 'markdown']; + + /** + * MarkdownText constructor. + * + * @param string $text + */ + public function __construct(string $text) + { + $this->add((new DeferredText(function () use ($text) { + return Markdown::text($text); + }))->setEscaped(true)); + } +} diff --git a/library/Icingadb/Widget/Notice.php b/library/Icingadb/Widget/Notice.php new file mode 100644 index 0000000..3ed6dad --- /dev/null +++ b/library/Icingadb/Widget/Notice.php @@ -0,0 +1,36 @@ +<?php + +/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; + +class Notice extends BaseHtmlElement +{ + /** @var mixed */ + protected $content; + + protected $tag = 'p'; + + protected $defaultAttributes = ['class' => 'notice']; + + /** + * Create a html notice + * + * @param mixed $content + */ + public function __construct($content) + { + $this->content = $content; + } + + protected function assemble(): void + { + $this->addHtml(new Icon('triangle-exclamation')); + $this->addHtml((new HtmlElement('span'))->add($this->content)); + $this->addHtml(new Icon('triangle-exclamation')); + } +} diff --git a/library/Icingadb/Widget/PluginOutputContainer.php b/library/Icingadb/Widget/PluginOutputContainer.php new file mode 100644 index 0000000..a8ff578 --- /dev/null +++ b/library/Icingadb/Widget/PluginOutputContainer.php @@ -0,0 +1,22 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Module\Icingadb\Util\PluginOutput; +use ipl\Html\BaseHtmlElement; + +class PluginOutputContainer extends BaseHtmlElement +{ + protected $tag = 'div'; + + public function __construct(PluginOutput $output) + { + $this->setHtmlContent($output); + + $this->getAttributes()->registerAttributeCallback('class', function () use ($output) { + return $output->isHtml() ? 'plugin-output' : 'plugin-output preformatted'; + }); + } +} diff --git a/library/Icingadb/Widget/ServiceStateBadges.php b/library/Icingadb/Widget/ServiceStateBadges.php new file mode 100644 index 0000000..fee2586 --- /dev/null +++ b/library/Icingadb/Widget/ServiceStateBadges.php @@ -0,0 +1,46 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Common\ServiceStates; +use Icinga\Module\Icingadb\Common\StateBadges; +use ipl\Web\Url; + +class ServiceStateBadges extends StateBadges +{ + protected function getBaseUrl(): Url + { + return Links::services(); + } + + protected function getType(): string + { + return 'service'; + } + + protected function getPrefix(): string + { + return 'services'; + } + + protected function getStateInt(string $state): int + { + return ServiceStates::int($state); + } + + protected function assemble() + { + $this->addAttributes(['class' => 'service-state-badges']); + + $this->add(array_filter([ + $this->createGroup('critical'), + $this->createGroup('warning'), + $this->createGroup('unknown'), + $this->createBadge('ok'), + $this->createBadge('pending') + ])); + } +} diff --git a/library/Icingadb/Widget/ServiceStatusBar.php b/library/Icingadb/Widget/ServiceStatusBar.php new file mode 100644 index 0000000..56f47aa --- /dev/null +++ b/library/Icingadb/Widget/ServiceStatusBar.php @@ -0,0 +1,24 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Module\Icingadb\Common\BaseStatusBar; +use ipl\Html\BaseHtmlElement; + +class ServiceStatusBar extends BaseStatusBar +{ + protected function assembleTotal(BaseHtmlElement $total): void + { + $total->add(sprintf( + tp('%d Service', '%d Services', $this->summary->services_total), + $this->summary->services_total + )); + } + + protected function createStateBadges(): BaseHtmlElement + { + return (new ServiceStateBadges($this->summary))->setBaseFilter($this->getBaseFilter()); + } +} diff --git a/library/Icingadb/Widget/ServiceSummaryDonut.php b/library/Icingadb/Widget/ServiceSummaryDonut.php new file mode 100644 index 0000000..e806fba --- /dev/null +++ b/library/Icingadb/Widget/ServiceSummaryDonut.php @@ -0,0 +1,81 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use Icinga\Chart\Donut; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Stdlib\BaseFilter; +use ipl\Stdlib\Filter; +use ipl\Web\Common\Card; + +class ServiceSummaryDonut extends Card +{ + use BaseFilter; + + protected $defaultAttributes = ['class' => 'donut-container', 'data-base-target' => '_next']; + + /** @var ServicestateSummary */ + protected $summary; + + public function __construct(ServicestateSummary $summary) + { + $this->summary = $summary; + } + + protected function assembleBody(BaseHtmlElement $body) + { + $labelBigUrlFilter = Filter::all( + Filter::equal('service.state.soft_state', 2), + Filter::equal('service.state.is_handled', 'n') + ); + if ($this->hasBaseFilter()) { + $labelBigUrlFilter->add($this->getBaseFilter()); + } + + $donut = (new Donut()) + ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) + ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) + ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) + ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) + ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) + ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) + ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) + ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']) + ->setLabelBig($this->summary->services_critical_unhandled) + ->setLabelBigUrl(Links::services()->setFilter($labelBigUrlFilter)->addParams([ + 'sort' => 'service.state.last_state_change' + ])) + ->setLabelBigEyeCatching($this->summary->services_critical_unhandled > 0) + ->setLabelSmall(t('Critical')); + + $body->addHtml( + new HtmlElement('div', Attributes::create(['class' => 'donut']), new HtmlString($donut->render())) + ); + } + + protected function assembleFooter(BaseHtmlElement $footer) + { + $footer->addHtml((new ServiceStateBadges($this->summary))->setBaseFilter($this->getBaseFilter())); + } + + protected function assembleHeader(BaseHtmlElement $header) + { + $header->addHtml( + new HtmlElement('h2', null, Text::create(t('Services'))), + new HtmlElement('span', Attributes::create(['class' => 'meta']), TemplateString::create( + t('{{#total}}Total{{/total}} %d'), + ['total' => new HtmlElement('span')], + (int) $this->summary->services_total + )) + ); + } +} diff --git a/library/Icingadb/Widget/ShowMore.php b/library/Icingadb/Widget/ShowMore.php new file mode 100644 index 0000000..d7fc7fb --- /dev/null +++ b/library/Icingadb/Widget/ShowMore.php @@ -0,0 +1,64 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Orm\ResultSet; +use ipl\Web\Common\BaseTarget; +use ipl\Web\Url; +use ipl\Web\Widget\ActionLink; + +class ShowMore extends BaseHtmlElement +{ + use BaseTarget; + + protected $defaultAttributes = ['class' => 'show-more']; + + protected $tag = 'div'; + + /** @var ResultSet */ + protected $resultSet; + + /** @var Url */ + protected $url; + + /** @var ?string */ + protected $label; + + public function __construct(ResultSet $resultSet, Url $url, string $label = null) + { + $this->label = $label; + $this->resultSet = $resultSet; + $this->url = $url; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getLabel(): string + { + return $this->label ?: t('Show More'); + } + + public function renderUnwrapped(): string + { + if ($this->resultSet->hasMore()) { + return parent::renderUnwrapped(); + } + + return ''; + } + + protected function assemble(): void + { + if ($this->resultSet->hasMore()) { + $this->addHtml(new ActionLink($this->getLabel(), $this->url)); + } + } +} diff --git a/library/Icingadb/Widget/StateBadge.php b/library/Icingadb/Widget/StateBadge.php new file mode 100644 index 0000000..d947590 --- /dev/null +++ b/library/Icingadb/Widget/StateBadge.php @@ -0,0 +1,10 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +/** @deprecated Use {@see \ipl\Web\Widget\StateBadge} instead */ +class StateBadge extends \ipl\Web\Widget\StateBadge +{ +} diff --git a/library/Icingadb/Widget/StateChange.php b/library/Icingadb/Widget/StateChange.php new file mode 100644 index 0000000..a9987be --- /dev/null +++ b/library/Icingadb/Widget/StateChange.php @@ -0,0 +1,133 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\StateBall; + +class StateChange extends BaseHtmlElement +{ + protected $previousState; + + protected $state; + + protected $previousStateBallSize = StateBall::SIZE_BIG; + + protected $currentStateBallSize = StateBall::SIZE_BIG; + + protected $defaultAttributes = ['class' => 'state-change']; + + protected $tag = 'div'; + + /** @var ?Icon Current state ball icon */ + protected $icon; + + /** @var bool Whether the state is handled */ + protected $isHandled = false; + + public function __construct(string $state, string $previousState) + { + $this->previousState = $previousState; + $this->state = $state; + } + + /** + * Set the state ball size for the previous state + * + * @param string $size + * + * @return $this + */ + public function setPreviousStateBallSize(string $size): self + { + $this->previousStateBallSize = $size; + + return $this; + } + + /** + * Set the state ball size for the current state + * + * @param string $size + * + * @return $this + */ + public function setCurrentStateBallSize(string $size): self + { + $this->currentStateBallSize = $size; + + return $this; + } + + /** + * Set the current state ball icon + * + * @param $icon + * + * @return $this + */ + public function setIcon($icon): self + { + $this->icon = $icon; + + return $this; + } + + /** + * Set whether the current state is handled + * + * @return $this + */ + public function setHandled($isHandled = true): self + { + $this->isHandled = $isHandled; + + return $this; + } + + protected function assemble() + { + $currentStateBall = (new StateBall($this->state, $this->currentStateBallSize)) + ->add($this->icon); + + if ($this->isHandled) { + $currentStateBall->getAttributes()->add('class', 'handled'); + } + + $previousStateBall = new StateBall($this->previousState, $this->previousStateBallSize); + if ($this->isRightBiggerThanLeft()) { + $this->getAttributes()->add('class', 'reversed-state-balls'); + + $this->addHtml($currentStateBall, $previousStateBall); + } else { + $this->addHtml($previousStateBall, $currentStateBall); + } + } + + protected function isRightBiggerThanLeft(): bool + { + $left = $this->previousStateBallSize; + $right = $this->currentStateBallSize; + + if ($left === $right) { + return false; + } elseif ($left === StateBall::SIZE_LARGE) { + return false; + } + + $map = [ + StateBall::SIZE_BIG => [false, [StateBall::SIZE_LARGE]], + StateBall::SIZE_MEDIUM_LARGE => [false, [StateBall::SIZE_BIG, StateBall::SIZE_LARGE]], + StateBall::SIZE_MEDIUM => [true, [StateBall::SIZE_TINY, StateBall::SIZE_SMALL]], + StateBall::SIZE_SMALL => [true, [StateBall::SIZE_TINY]] + ]; + + list($negate, $sizes) = $map[$left]; + $found = in_array($right, $sizes, true); + + return ($negate && ! $found) || (! $negate && $found); + } +} diff --git a/library/Icingadb/Widget/TagList.php b/library/Icingadb/Widget/TagList.php new file mode 100644 index 0000000..6a28a9c --- /dev/null +++ b/library/Icingadb/Widget/TagList.php @@ -0,0 +1,35 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Web\Widget\Link; + +class TagList extends BaseHtmlElement +{ + protected $content = []; + + protected $defaultAttributes = ['class' => 'tag-list']; + + protected $tag = 'div'; + + public function addLink($content, $url): self + { + $this->content[] = new Link($content, $url); + + return $this; + } + + public function hasContent(): bool + { + return ! empty($this->content); + } + + protected function assemble() + { + $this->add(Html::wrapEach($this->content, 'li')); + } +} |