summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/web/src/Widget
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/web/src/Widget')
-rw-r--r--vendor/ipl/web/src/Widget/ActionBar.php51
-rw-r--r--vendor/ipl/web/src/Widget/ActionLink.php31
-rw-r--r--vendor/ipl/web/src/Widget/ButtonLink.php14
-rw-r--r--vendor/ipl/web/src/Widget/ContinueWith.php72
-rw-r--r--vendor/ipl/web/src/Widget/CopyToClipboard.php64
-rw-r--r--vendor/ipl/web/src/Widget/Dropdown.php63
-rw-r--r--vendor/ipl/web/src/Widget/EmptyState.php30
-rw-r--r--vendor/ipl/web/src/Widget/EmptyStateBar.php30
-rw-r--r--vendor/ipl/web/src/Widget/HorizontalKeyValue.php31
-rw-r--r--vendor/ipl/web/src/Widget/IcingaIcon.php28
-rw-r--r--vendor/ipl/web/src/Widget/Icon.php67
-rw-r--r--vendor/ipl/web/src/Widget/Link.php97
-rw-r--r--vendor/ipl/web/src/Widget/StateBadge.php47
-rw-r--r--vendor/ipl/web/src/Widget/StateBall.php43
-rw-r--r--vendor/ipl/web/src/Widget/Tabs.php190
-rw-r--r--vendor/ipl/web/src/Widget/TimeAgo.php33
-rw-r--r--vendor/ipl/web/src/Widget/TimeSince.php33
-rw-r--r--vendor/ipl/web/src/Widget/TimeUntil.php34
-rw-r--r--vendor/ipl/web/src/Widget/VerticalKeyValue.php32
19 files changed, 990 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/Widget/ActionBar.php b/vendor/ipl/web/src/Widget/ActionBar.php
new file mode 100644
index 0000000..bf31845
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/ActionBar.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Web\Common\BaseTarget;
+use ipl\Web\Url;
+
+/**
+ * Action bar element for displaying a list of links
+ */
+class ActionBar extends BaseHtmlElement
+{
+ use BaseTarget;
+
+ protected $contentSeparator = ' ';
+
+ protected $defaultAttributes = [
+ 'class' => 'action-bar',
+ 'data-base-target' => '_self'
+ ];
+
+ protected $tag = 'div';
+
+ /**
+ * Create a action bar
+ *
+ * @param Attributes|array $attributes
+ */
+ public function __construct($attributes = null)
+ {
+ $this->getAttributes()->add($attributes);
+ }
+
+ /**
+ * Add a link to the action bar
+ *
+ * @param mixed $content
+ * @param Url|string $url
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addLink($content, $url, $icon = null)
+ {
+ $this->add(new ActionLink($content, $url, $icon));
+
+ return $this;
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/ActionLink.php b/vendor/ipl/web/src/Widget/ActionLink.php
new file mode 100644
index 0000000..289d700
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/ActionLink.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Web\Url;
+
+/**
+ * Link generally pointing to CRUD actions
+ */
+class ActionLink extends Link
+{
+ protected $defaultAttributes = ['class' => 'action-link'];
+
+ /**
+ * Create a action link
+ *
+ * @param mixed $content
+ * @param Url|string $url
+ * @param string $icon
+ * @param Attributes|array $attributes
+ */
+ public function __construct($content, $url, $icon = null, $attributes = null)
+ {
+ parent::__construct($content, $url, $attributes);
+
+ if ($icon !== null) {
+ $this->prepend(new Icon($icon));
+ }
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/ButtonLink.php b/vendor/ipl/web/src/Widget/ButtonLink.php
new file mode 100644
index 0000000..2da5dfd
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/ButtonLink.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+/**
+ * Button like link generally pointing to CRUD actions
+ */
+class ButtonLink extends ActionLink
+{
+ protected $defaultAttributes = [
+ 'class' => 'button-link',
+ 'data-base-target' => '_main'
+ ];
+}
diff --git a/vendor/ipl/web/src/Widget/ContinueWith.php b/vendor/ipl/web/src/Widget/ContinueWith.php
new file mode 100644
index 0000000..1479e9a
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/ContinueWith.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\Stdlib\Filter;
+use ipl\Web\Common\BaseTarget;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class ContinueWith extends BaseHtmlElement
+{
+ use BaseTarget;
+
+ protected $tag = 'span';
+
+ protected $defaultAttributes = ['class' => 'continue-with'];
+
+ /** @var Url */
+ protected $url;
+
+ /** @var Filter\Rule|callable */
+ protected $filter;
+
+ /** @var string */
+ protected $title;
+
+ public function __construct(Url $url, $filter)
+ {
+ $this->url = $url;
+ $this->filter = $filter;
+ }
+
+ /**
+ * Set title for the anchor
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ public function assemble()
+ {
+ $filter = $this->filter;
+ if (is_callable($filter)) {
+ $filter = $filter(); /** @var Filter\Rule $filter */
+ }
+
+ if ($filter instanceof Filter\Chain && $filter->isEmpty()) {
+ $this->addHtml(new HtmlElement(
+ 'span',
+ Attributes::create(['class' => ['control-button', 'disabled']]),
+ new Icon('share')
+ ));
+ } else {
+ $this->addHtml(new ActionLink(
+ null,
+ $this->url->setFilter($filter),
+ 'share',
+ ['class' => 'control-button', 'title' => $this->title]
+ ));
+ }
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/CopyToClipboard.php b/vendor/ipl/web/src/Widget/CopyToClipboard.php
new file mode 100644
index 0000000..28e9347
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/CopyToClipboard.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\I18n\Translation;
+
+/**
+ * Copy to clipboard button
+ */
+class CopyToClipboard extends BaseHtmlElement
+{
+ use Translation;
+
+ protected $tag = 'button';
+
+ protected $defaultAttributes = ['type' => 'button'];
+
+ /**
+ * Create a copy to clipboard button
+ *
+ * Creates a copy to clipboard button, which when clicked copies the text from the html element identified as
+ * clipboard source that the clipboard button attaches itself to.
+ */
+ private function __construct()
+ {
+ $this->addAttributes(
+ [
+ 'class' => 'copy-to-clipboard',
+ 'data-icinga-clipboard' => true,
+ 'tabindex' => -1,
+ 'data-copied-label' => $this->translate('Copied'),
+ 'title' => $this->translate('Copy to clipboard'),
+ ]
+ );
+ }
+
+ /**
+ * Attach the copy to clipboard button to the given Html source element
+ *
+ * @param BaseHtmlElement $source
+ *
+ * @return void
+ */
+ public static function attachTo(BaseHtmlElement $source): void
+ {
+ $clipboardWrapper = new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'clipboard-wrapper'])
+ );
+
+ $clipboardWrapper->addHtml(new static());
+
+ $source->addAttributes(['data-clipboard-source' => true]);
+ $source->prependWrapper($clipboardWrapper);
+ }
+
+ public function assemble(): void
+ {
+ $this->setHtmlContent(new Icon('clone'));
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/Dropdown.php b/vendor/ipl/web/src/Widget/Dropdown.php
new file mode 100644
index 0000000..b6eb20d
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/Dropdown.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Url;
+
+/**
+ * Toggleable overlay dropdown element for displaying a list of links
+ */
+class Dropdown extends BaseHtmlElement
+{
+ /** @var array */
+ protected $links = [];
+
+ protected $defaultAttributes = ['class' => 'dropdown'];
+
+ protected $tag = 'div';
+
+ /**
+ * Create a dropdown element
+ *
+ * @param mixed $content
+ * @param Attributes|array $attributes
+ */
+ public function __construct($content, $attributes = null)
+ {
+ $toggle = new ActionLink($content, '#', null, [
+ 'aria-expanded' => false,
+ 'aria-haspopup' => true,
+ 'class' => 'dropdown-toggle',
+ 'role' => 'button'
+ ]);
+
+ $this
+ ->setContent($toggle)
+ ->getAttributes()
+ ->add($attributes);
+ }
+
+ /**
+ * Add a link to the dropdown
+ *
+ * @param mixed $content
+ * @param Url|string $url
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addLink($content, $url, $icon = null)
+ {
+ $this->links[] = new ActionLink($content, $url, $icon, ['class' => 'dropdown-item']);
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->add(Html::tag('div', ['class' => 'dropdown-menu'], $this->links));
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/EmptyState.php b/vendor/ipl/web/src/Widget/EmptyState.php
new file mode 100644
index 0000000..5a055ac
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/EmptyState.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+
+class EmptyState extends BaseHtmlElement
+{
+ /** @var mixed Content */
+ protected $content;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'empty-state'];
+
+ /**
+ * Create an empty state
+ *
+ * @param mixed $content
+ */
+ public function __construct($content)
+ {
+ $this->content = $content;
+ }
+
+ protected function assemble(): void
+ {
+ $this->add($this->content);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/EmptyStateBar.php b/vendor/ipl/web/src/Widget/EmptyStateBar.php
new file mode 100644
index 0000000..2d04837
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/EmptyStateBar.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+
+class EmptyStateBar extends BaseHtmlElement
+{
+ /** @var mixed Content */
+ protected $content;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'empty-state-bar'];
+
+ /**
+ * Create an empty list
+ *
+ * @param mixed $content
+ */
+ public function __construct($content)
+ {
+ $this->content = $content;
+ }
+
+ protected function assemble(): void
+ {
+ $this->add($this->content);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/HorizontalKeyValue.php b/vendor/ipl/web/src/Widget/HorizontalKeyValue.php
new file mode 100644
index 0000000..1d1195e
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/HorizontalKeyValue.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class HorizontalKeyValue extends BaseHtmlElement
+{
+ protected $key;
+
+ protected $value;
+
+ protected $defaultAttributes = ['class' => 'horizontal-key-value'];
+
+ protected $tag = 'div';
+
+ public function __construct($key, $value)
+ {
+ $this->key = $key;
+ $this->value = $value;
+ }
+
+ protected function assemble()
+ {
+ $this->add([
+ Html::tag('div', ['class' => 'key'], $this->key),
+ Html::tag('div', ['class' => 'value'], $this->value)
+ ]);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/IcingaIcon.php b/vendor/ipl/web/src/Widget/IcingaIcon.php
new file mode 100644
index 0000000..1161fc6
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/IcingaIcon.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+
+class IcingaIcon extends Icon
+{
+ protected $style = '';
+
+ /**
+ * Create an icon element
+ *
+ * Creates an icon element from the given name and HTML attributes. The icon element's tag will be <i>. The given
+ * name will be used as automatically added CSS class for the icon element in the format 'iicon-$name'. In addition,
+ * the CSS class 'icon' will be automatically added too.
+ *
+ * @param string $name The name of the icon
+ * @param Attributes|array $attributes The HTML attributes for the element
+ */
+ public function __construct(string $name, $attributes = null)
+ {
+ $this
+ ->getAttributes()
+ ->add('class', ['icon', "iicon-$name"])
+ ->add($attributes);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/Icon.php b/vendor/ipl/web/src/Widget/Icon.php
new file mode 100644
index 0000000..5c2617f
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/Icon.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+
+/**
+ * Icon element
+ */
+class Icon extends BaseHtmlElement
+{
+ protected $tag = 'i';
+
+ /** @var string Icon style */
+ protected $style;
+
+ /** @var string Icon default style */
+ protected $defaultStyle = 'fa';
+
+ /**
+ * Create an icon element
+ *
+ * Creates an icon element from the given name and HTML attributes. The icon element's tag will be <i>. The given
+ * name will be used as automatically added CSS class for the icon element in the format 'icon-$name'. In addition,
+ * the CSS class 'icon' will be automatically added too.
+ *
+ * @param string $name The name of the icon
+ * @param Attributes|array $attributes The HTML attributes for the element
+ */
+ public function __construct(string $name, $attributes = null)
+ {
+ $this
+ ->getAttributes()
+ ->add('class', ['icon', "fa-$name"])
+ ->add($attributes);
+ }
+
+ /**
+ * Get the icon style
+ *
+ * @return string
+ */
+ public function getStyle(): string
+ {
+ return $this->style ?? $this->defaultStyle;
+ }
+
+ /**
+ * Set the icon style
+ *
+ * @param string $style Style class with prefix
+ *
+ * @return $this
+ */
+ public function setStyle(string $style): self
+ {
+ $this->style = $style;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->addAttributes(['class' => $this->getStyle()]);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/Link.php b/vendor/ipl/web/src/Widget/Link.php
new file mode 100644
index 0000000..cbae3b9
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/Link.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\Attribute;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Web\Common\BaseTarget;
+use ipl\Web\Url;
+
+/**
+ * Link element, i.e. <a href="...
+ */
+class Link extends BaseHtmlElement
+{
+ use BaseTarget;
+
+ /** @var Url */
+ protected $url;
+
+ protected $tag = 'a';
+
+ /**
+ * Create a link element
+ *
+ * @param mixed $content
+ * @param Url|string $url
+ * @param Attributes|array $attributes
+ */
+ public function __construct($content, $url, $attributes = null)
+ {
+ $this
+ ->setContent($content)
+ ->setUrl($url)
+ ->getAttributes()
+ ->add($attributes)
+ ->registerAttributeCallback('href', [$this, 'createHrefAttribute']);
+ }
+
+ /**
+ * Get the URL of the link
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the URL of the link
+ *
+ * @param Url|string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ if (! $url instanceof Url) {
+ try {
+ $url = Url::fromPath($url);
+ } catch (\Exception $e) {
+ $url = 'invalid';
+ }
+ }
+
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Create and return the href attribute
+ *
+ * Used as attribute callback for the href attribute.
+ *
+ * @return Attribute
+ */
+ public function createHrefAttribute()
+ {
+ return new Attribute('href', (string) $this->getUrl());
+ }
+
+ /**
+ * Open this link in a modal
+ *
+ * @return $this
+ */
+ public function openInModal(): self
+ {
+ $this->getAttributes()
+ ->set('data-icinga-modal', true)
+ ->set('data-no-icinga-ajax', true);
+
+ return $this;
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/StateBadge.php b/vendor/ipl/web/src/Widget/StateBadge.php
new file mode 100644
index 0000000..908a348
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/StateBadge.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+
+class StateBadge extends BaseHtmlElement
+{
+ protected $defaultAttributes = ['class' => 'state-badge'];
+
+ /** @var mixed Badge content */
+ protected $content;
+
+ /** @var bool Whether the state is handled */
+ protected $isHandled;
+
+ /** @var string Textual representation of a state */
+ protected $state;
+
+ /**
+ * Create a new state badge
+ *
+ * @param mixed $content Content of the badge
+ * @param string $state Textual representation of a state
+ * @param bool $isHandled True if state is handled
+ */
+ public function __construct($content, string $state, bool $isHandled = false)
+ {
+ $this->content = $content;
+ $this->isHandled = $isHandled;
+ $this->state = $state;
+ }
+
+ protected function assemble()
+ {
+ $this->setTag('span');
+
+ $class = "state-{$this->state}";
+ if ($this->isHandled) {
+ $class .= ' handled';
+ }
+
+ $this->addAttributes(['class' => $class]);
+
+ $this->add($this->content);
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/StateBall.php b/vendor/ipl/web/src/Widget/StateBall.php
new file mode 100644
index 0000000..5a1216d
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/StateBall.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+
+/**
+ * State ball element that supports different sizes and colors
+ */
+class StateBall extends BaseHtmlElement
+{
+ const SIZE_TINY = 'xs';
+ const SIZE_SMALL = 's';
+ const SIZE_MEDIUM = 'm';
+ const SIZE_MEDIUM_LARGE = 'ml';
+ const SIZE_BIG = 'l';
+ const SIZE_LARGE = 'xl';
+
+ protected $tag = 'span';
+
+ /**
+ * Create a new state ball element
+ *
+ * @param string $state
+ * @param string $size
+ */
+ public function __construct($state = 'none', $size = self::SIZE_SMALL)
+ {
+ $state = trim($state);
+
+ if (empty($state)) {
+ $state = 'none';
+ }
+
+ $size = trim($size);
+
+ if (empty($size)) {
+ $size = self::SIZE_MEDIUM;
+ }
+
+ $this->defaultAttributes = ['class' => "state-ball state-$state ball-size-$size"];
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/Tabs.php b/vendor/ipl/web/src/Widget/Tabs.php
new file mode 100644
index 0000000..32ba8e9
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/Tabs.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use Exception;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use Icinga\Web\Widget\Tabextension\Tabextension;
+use InvalidArgumentException;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Web\Url;
+
+/**
+ * @TODO(el): Don't depend on Icinga Web's Tabs
+ */
+class Tabs extends BaseHtmlElement
+{
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = ['class' => 'tabs primary-nav nav'];
+
+ /** @var \Icinga\Web\Widget\Tabs */
+ protected $tabs;
+
+ /** @var bool Whether data exports are enabled */
+ protected $dataExportsEnabled = false;
+
+ /** @var bool Whether the legacy extensions should be shown by default */
+ protected $legacyExtensionsEnabled = true;
+
+ /** @var Url */
+ protected $refreshUrl;
+
+ public function __construct()
+ {
+ $this->tabs = new \Icinga\Web\Widget\Tabs();
+ }
+
+ /**
+ * Don't show legacy extensions by default
+ */
+ public function disableLegacyExtensions()
+ {
+ $this->legacyExtensionsEnabled = false;
+ }
+
+ /**
+ * Show export actions for JSON and CSV
+ */
+ public function enableDataExports()
+ {
+ $this->dataExportsEnabled = true;
+ }
+
+ /**
+ * Set the url for the refresh button
+ *
+ * @param Url $url
+ *
+ * @return $this
+ */
+ public function setRefreshUrl(Url $url)
+ {
+ $this->refreshUrl = $url;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ if ($this->legacyExtensionsEnabled) {
+ $this->tabs->extend(new OutputFormat(
+ $this->dataExportsEnabled
+ ? []
+ : [OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON]
+ ))
+ ->extend(new DashboardAction())
+ ->extend(new MenuAction());
+ }
+
+ $tabHtml = substr($this->tabs->render(), 34, -5);
+ if ($this->refreshUrl !== null) {
+ $tabHtml = preg_replace(
+ '/(?<=class="refresh-container-control spinner" href=")([^"]*)/',
+ $this->refreshUrl->getAbsoluteUrl(),
+ $tabHtml
+ );
+ }
+
+ parent::add(HtmlString::create($tabHtml));
+ }
+
+ /**
+ * Activate the tab with the given name
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException
+ */
+ public function activate($name)
+ {
+ try {
+ $this->tabs->activate($name);
+ } catch (Exception $e) {
+ throw new InvalidArgumentException($e->getMessage());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get active tab
+ *
+ * @return \Icinga\Web\Widget\Tab
+ */
+ public function getActiveTab()
+ {
+ return $this->tabs->get($this->tabs->getActiveName());
+ }
+
+ /**
+ * Add the given tab
+ *
+ * @param string $name
+ * @param mixed $tab
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException
+ */
+ public function add($name, $tab = null)
+ {
+ if ($tab === null) {
+ throw new InvalidArgumentException('Argument $tab is required');
+ }
+
+ try {
+ $this->tabs->add($name, $tab);
+ } catch (Exception $e) {
+ throw new InvalidArgumentException($e->getMessage());
+ }
+
+ if (is_array($tab) && isset($tab['active']) && $tab['active']) {
+ // Otherwise Tabs::getActiveName() returns null
+ $this->tabs->activate($name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get a tab
+ *
+ * @param string $name
+ *
+ * @return \Icinga\Web\Widget\Tab|null
+ */
+ public function get($name)
+ {
+ return $this->tabs->get($name);
+ }
+
+ /**
+ * Count tabs
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->tabs->count();
+ }
+
+ /**
+ * Apply a Tabextension on $this->tabs object not on this class
+ *
+ * @param Tabextension $extension
+ *
+ * @return $this
+ */
+ public function extend(Tabextension $extension)
+ {
+ $this->tabs->extend($extension);
+
+ return $this;
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/TimeAgo.php b/vendor/ipl/web/src/Widget/TimeAgo.php
new file mode 100644
index 0000000..cbd0dad
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/TimeAgo.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use Icinga\Date\DateFormatter;
+use ipl\Html\BaseHtmlElement;
+
+class TimeAgo extends BaseHtmlElement
+{
+ /** @var int */
+ protected $ago;
+
+ protected $tag = 'time';
+
+ protected $defaultAttributes = ['class' => 'time-ago'];
+
+ public function __construct($ago)
+ {
+ $this->ago = (int) $ago;
+ }
+
+ protected function assemble()
+ {
+ $dateTime = DateFormatter::formatDateTime($this->ago);
+
+ $this->addAttributes([
+ 'datetime' => $dateTime,
+ 'title' => $dateTime
+ ]);
+
+ $this->add(DateFormatter::timeAgo($this->ago));
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/TimeSince.php b/vendor/ipl/web/src/Widget/TimeSince.php
new file mode 100644
index 0000000..308e358
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/TimeSince.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use Icinga\Date\DateFormatter;
+use ipl\Html\BaseHtmlElement;
+
+class TimeSince extends BaseHtmlElement
+{
+ /** @var int */
+ protected $since;
+
+ protected $tag = 'time';
+
+ protected $defaultAttributes = ['class' => 'time-since'];
+
+ public function __construct($since)
+ {
+ $this->since = (int) $since;
+ }
+
+ protected function assemble()
+ {
+ $dateTime = DateFormatter::formatDateTime($this->since);
+
+ $this->addAttributes([
+ 'datetime' => $dateTime,
+ 'title' => $dateTime
+ ]);
+
+ $this->add(DateFormatter::timeSince($this->since));
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/TimeUntil.php b/vendor/ipl/web/src/Widget/TimeUntil.php
new file mode 100644
index 0000000..f16731a
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/TimeUntil.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use Icinga\Date\DateFormatter;
+use ipl\Html\BaseHtmlElement;
+
+class TimeUntil extends BaseHtmlElement
+{
+ /** @var int */
+ protected $until;
+
+ protected $tag = 'time';
+
+ protected $defaultAttributes = ['class' => 'time-until'];
+
+ public function __construct($until)
+ {
+ $this->until = (int) $until;
+ }
+
+ protected function assemble()
+ {
+ $dateTime = DateFormatter::formatDateTime($this->until);
+
+ $this->addAttributes([
+ 'datetime' => $dateTime,
+ 'title' => $dateTime,
+ 'data-ago-label' => DateFormatter::timeAgo(time())
+ ]);
+
+ $this->add(DateFormatter::timeUntil($this->until));
+ }
+}
diff --git a/vendor/ipl/web/src/Widget/VerticalKeyValue.php b/vendor/ipl/web/src/Widget/VerticalKeyValue.php
new file mode 100644
index 0000000..388c740
--- /dev/null
+++ b/vendor/ipl/web/src/Widget/VerticalKeyValue.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace ipl\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class VerticalKeyValue extends BaseHtmlElement
+{
+ protected $key;
+
+ protected $value;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'vertical-key-value'];
+
+ public function __construct($key, $value)
+ {
+ $this->key = $key;
+ $this->value = $value;
+ }
+
+ protected function assemble()
+ {
+ $this->add([
+ Html::tag('span', ['class' => 'value'], $this->value),
+ Html::tag('br'),
+ Html::tag('span', ['class' => 'key'], $this->key),
+ ]);
+ }
+}