summaryrefslogtreecommitdiffstats
path: root/library/Businessprocess/Renderer
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Businessprocess/Renderer/Breadcrumb.php78
-rw-r--r--library/Businessprocess/Renderer/Renderer.php398
-rw-r--r--library/Businessprocess/Renderer/TileRenderer.php90
-rw-r--r--library/Businessprocess/Renderer/TileRenderer/NodeTile.php357
-rw-r--r--library/Businessprocess/Renderer/TreeRenderer.php357
5 files changed, 1280 insertions, 0 deletions
diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php
new file mode 100644
index 0000000..56c41aa
--- /dev/null
+++ b/library/Businessprocess/Renderer/Breadcrumb.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class Breadcrumb extends BaseHtmlElement
+{
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = array(
+ 'class' => 'breadcrumb',
+ 'data-base-target' => '_main'
+ );
+
+ /**
+ * @param Renderer $renderer
+ * @return static
+ */
+ public static function create(Renderer $renderer)
+ {
+ $bp = $renderer->getBusinessProcess();
+ $breadcrumb = new static;
+ $bpUrl = $renderer->getBaseUrl();
+ if ($bpUrl->getParam('action') === 'delete') {
+ $bpUrl->remove('action');
+ }
+
+ $breadcrumb->add(Html::tag('li')->add(
+ Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess'),
+ 'title' => mt('businessprocess', 'Show Overview')
+ ],
+ Html::tag('i', ['class' => 'icon icon-home'])
+ )
+ ));
+ $breadcrumb->add(Html::tag('li')->add(
+ Html::tag('a', ['href' => $bpUrl], $bp->getTitle())
+ ));
+ $path = $renderer->getCurrentPath();
+
+ $parts = array();
+ while ($nodeName = array_pop($path)) {
+ $node = $bp->getNode($nodeName);
+ $renderer->setParentNode($node);
+ array_unshift(
+ $parts,
+ static::renderNode($node, $path, $renderer)
+ );
+ }
+ $breadcrumb->add($parts);
+
+ return $breadcrumb;
+ }
+
+ /**
+ * @param BpNode $node
+ * @param array $path
+ * @param Renderer $renderer
+ *
+ * @return NodeTile
+ */
+ protected static function renderNode(BpNode $node, $path, Renderer $renderer)
+ {
+ // TODO: something more generic than NodeTile?
+ $renderer = clone($renderer);
+ $renderer->lock()->setIsBreadcrumb();
+ $p = new NodeTile($renderer, $node, $path);
+ $p->setTag('li');
+ return $p;
+ }
+}
diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php
new file mode 100644
index 0000000..58e1c4d
--- /dev/null
+++ b/library/Businessprocess/Renderer/Renderer.php
@@ -0,0 +1,398 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Web\Widget\StateBadge;
+
+abstract class Renderer extends HtmlDocument
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var BpNode */
+ protected $parent;
+
+ /** @var bool Administrative actions are hidden unless unlocked */
+ protected $locked = true;
+
+ /** @var Url */
+ protected $url;
+
+ /** @var Url */
+ protected $baseUrl;
+
+ /** @var array */
+ protected $path = array();
+
+ /** @var bool */
+ protected $isBreadcrumb = false;
+
+ /**
+ * Renderer constructor.
+ *
+ * @param BpConfig $config
+ * @param BpNode|null $parent
+ */
+ public function __construct(BpConfig $config, BpNode $parent = null)
+ {
+ $this->config = $config;
+ $this->parent = $parent;
+ }
+
+ /**
+ * @return BpConfig
+ */
+ public function getBusinessProcess()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Whether this will render all root nodes
+ *
+ * @return bool
+ */
+ public function wantsRootNodes()
+ {
+ return $this->parent === null;
+ }
+
+ /**
+ * Whether this will only render parts of given config
+ *
+ * @return bool
+ */
+ public function rendersSubNode()
+ {
+ return $this->parent !== null;
+ }
+
+ public function rendersImportedNode()
+ {
+ return $this->parent !== null && $this->parent->getBpConfig()->getName() !== $this->config->getName();
+ }
+
+ public function setParentNode(BpNode $node)
+ {
+ $this->parent = $node;
+ return $this;
+ }
+
+ /**
+ * @return BpNode
+ */
+ public function getParentNode()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getParentNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return array();
+ }
+
+ return $this->parent->getParents();
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getChildNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return $this->config->getRootNodes();
+ } else {
+ return $this->parent->getChildren();
+ }
+ }
+
+ /**
+ * @return int
+ */
+ public function countChildNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return $this->config->countChildren();
+ } else {
+ return $this->parent->countChildren();
+ }
+ }
+
+ /**
+ * @param $summary
+ * @return BaseHtmlElement
+ */
+ public function renderStateBadges($summary, $totalChildren)
+ {
+ $elements = [];
+
+ $itemCount = Html::tag(
+ 'span',
+ [
+ 'class' => [
+ 'item-count',
+ ]
+ ],
+ sprintf(mtp('businessprocess', '%u Child', '%u Children', $totalChildren), $totalChildren)
+ );
+
+ $elements[] = array_filter([
+ $this->createBadgeGroup($summary, 'CRITICAL'),
+ $this->createBadgeGroup($summary, 'UNKNOWN'),
+ $this->createBadgeGroup($summary, 'WARNING'),
+ $this->createBadge($summary, 'MISSING'),
+ $this->createBadge($summary, 'PENDING')
+ ]);
+
+ if (!empty($elements)) {
+ $container = Html::tag('ul', ['class' => 'state-badges']);
+ $container->add($itemCount);
+ foreach ($elements as $element) {
+ $container->add($element);
+ }
+
+ return $container;
+ }
+ return null;
+ }
+
+ protected function createBadge($summary, $state)
+ {
+ if ($summary[$state] !== 0) {
+ return Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ return null;
+ }
+
+ protected function createBadgeGroup($summary, $state)
+ {
+ $content = [];
+ if ($summary[$state] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ if ($summary[$state . '-HANDLED'] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state . '-HANDLED'], strtolower($state), true));
+ }
+
+ if (empty($content)) {
+ return null;
+ }
+
+ return Html::tag('li', Html::tag('ul', $content));
+ }
+
+ public function getNodeClasses(Node $node)
+ {
+ if ($node->isMissing()) {
+ $classes = array('missing');
+ } else {
+ if ($node->isEmpty() && ! $node instanceof MonitoredNode) {
+ $classes = array('empty');
+ } else {
+ $classes = [strtolower($node->getStateName(
+ $this->parent !== null ? $this->parent->getChildState($node) : null
+ ))];
+ }
+ if ($node->hasMissingChildren()) {
+ $classes[] = 'missing-children';
+ }
+ }
+
+ if ($node->isHandled()) {
+ $classes[] = 'handled';
+ }
+
+ if ($node instanceof BpNode) {
+ $classes[] = 'process-node';
+ } else {
+ $classes[] = 'monitored-node';
+ }
+ // TODO: problem?
+ return $classes;
+ }
+
+ /**
+ * Return the url to the given node's source configuration
+ *
+ * @param BpNode $node
+ *
+ * @return Url
+ */
+ public function getSourceUrl(BpNode $node)
+ {
+ if ($node instanceof ImportedNode) {
+ $name = $node->getNodeName();
+ $paths = $node->getBpConfig()->getBpNode($name)->getPaths();
+ } else {
+ $name = $node->getName();
+ $paths = $node->getPaths();
+ }
+
+ $url = clone $this->getUrl();
+ $url->setParams([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $name
+ ]);
+ // This depends on the fact that the node's root path is the last element in $paths
+ $url->getParams()->addValues('path', array_slice(array_pop($paths), 0, -1));
+ if (! $this->isLocked()) {
+ $url->getParams()->add('unlocked', true);
+ }
+
+ return $url;
+ }
+
+ /**
+ * @param Node $node
+ * @param $path
+ * @return string
+ */
+ public function getId(Node $node, $path)
+ {
+ return md5((empty($path) ? '' : implode(';', $path)) . $node->getName());
+ }
+
+ public function setPath(array $path)
+ {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function getCurrentPath()
+ {
+ $path = $this->getPath();
+ if ($this->rendersSubNode()) {
+ $path[] = $this->rendersImportedNode()
+ ? $this->parent->getIdentifier()
+ : $this->parent->getName();
+ }
+
+ return $path;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ public function setUrl(Url $url)
+ {
+ $this->url = $url->without(array(
+ 'action',
+ 'deletenode',
+ 'deleteparent',
+ 'editnode',
+ 'simulationnode',
+ 'view'
+ ));
+ $this->setBaseUrl($this->url);
+ return $this;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ protected function setBaseUrl(Url $url)
+ {
+ $this->baseUrl = $url->without(array('node', 'path'));
+ return $this;
+ }
+
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return Url
+ * @throws ProgrammingError
+ */
+ public function getBaseUrl()
+ {
+ if ($this->baseUrl === null) {
+ throw new ProgrammingError('Renderer has no baseUrl');
+ }
+
+ return clone($this->baseUrl);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isLocked()
+ {
+ return $this->locked;
+ }
+
+ /**
+ * @return $this
+ */
+ public function lock()
+ {
+ $this->locked = true;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function unlock()
+ {
+ $this->locked = false;
+ return $this;
+ }
+
+ /**
+ * TODO: Get rid of this
+ *
+ * @return $this
+ */
+ public function setIsBreadcrumb()
+ {
+ $this->isBreadcrumb = true;
+ return $this;
+ }
+
+ public function isBreadcrumb()
+ {
+ return $this->isBreadcrumb;
+ }
+
+ protected function createUnboundParent(BpConfig $bp)
+ {
+ return $bp->getNode('__unbound__');
+ }
+
+ /**
+ * Just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->parent);
+ unset($this->config);
+ }
+}
diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php
new file mode 100644
index 0000000..f1f779e
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use ipl\Html\Html;
+
+class TileRenderer extends Renderer
+{
+ /**
+ * @inheritdoc
+ */
+ public function render()
+ {
+ $bp = $this->config;
+ $nodesDiv = Html::tag(
+ 'div',
+ [
+ 'class' => ['sortable', 'tiles', $this->howMany()],
+ 'data-base-target' => '_self',
+ 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists
+ 'data-csrf-token' => CsrfToken::generate()
+ ]
+ );
+
+ if ($this->wantsRootNodes()) {
+ $nodesDiv->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $nodesDiv->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $nodes = $this->getChildNodes();
+
+ $path = $this->getCurrentPath();
+ foreach ($nodes as $name => $node) {
+ $this->add(new NodeTile($this, $node, $path));
+ }
+
+ if ($this->wantsRootNodes()) {
+ $unbound = $this->createUnboundParent($bp);
+ if ($unbound->hasChildren()) {
+ $this->add(new NodeTile($this, $unbound));
+ }
+ }
+
+ $nodesDiv->add($this->getContent());
+ $this->setContent($nodesDiv);
+
+ return parent::render();
+ }
+
+ /**
+ * A CSS class giving a rough indication of how many nodes we have
+ *
+ * This is used to show larger tiles when there are few and smaller
+ * ones if there are many.
+ *
+ * @return string
+ */
+ protected function howMany()
+ {
+ $count = $this->countChildNodes();
+ $howMany = 'normal';
+
+ if ($count <= 6) {
+ $howMany = 'few';
+ } elseif ($count > 12) {
+ $howMany = 'many';
+ }
+
+ return $howMany;
+ }
+}
diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
new file mode 100644
index 0000000..67bb4a6
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer\TileRenderer;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\HostNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\StateBall;
+
+class NodeTile extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $renderer;
+
+ protected $name;
+
+ protected $node;
+
+ protected $path;
+
+ /**
+ * @var BaseHtmlElement
+ */
+ private $actions;
+
+ /**
+ * NodeTile constructor.
+ * @param Renderer $renderer
+ * @param $name
+ * @param Node $node
+ * @param null $path
+ */
+ public function __construct(Renderer $renderer, Node $node, $path = null)
+ {
+ $this->renderer = $renderer;
+ $this->node = $node;
+ $this->path = $path;
+ }
+
+ protected function actions()
+ {
+ if ($this->actions === null) {
+ $this->addActions();
+ }
+ return $this->actions;
+ }
+
+ protected function addActions()
+ {
+ $this->actions = Html::tag(
+ 'div',
+ [
+ 'class' => 'actions'
+ ]
+ );
+
+ return $this->add($this->actions);
+ }
+
+ public function render()
+ {
+ $renderer = $this->renderer;
+ $node = $this->node;
+
+ $attributes = $this->getAttributes();
+ $attributes->add('class', $renderer->getNodeClasses($node));
+ $attributes->add('id', $renderer->getId($node, $this->path));
+ if (! $renderer->isLocked()) {
+ $attributes->add('data-node-name', $node->getName());
+ }
+
+ if (! $renderer->isBreadcrumb()) {
+ $this->addDetailsActions();
+
+ if (! $renderer->isLocked()) {
+ $this->addActionLinks();
+ }
+ }
+ if (! $node instanceof ImportedNode || $node->getBpConfig()->hasNode($node->getName())) {
+ $link = $this->getMainNodeLink();
+ if ($renderer->isBreadcrumb()) {
+ $state = strtolower($node->getStateName());
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $link->prepend((new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]));
+ }
+
+ $this->add($link);
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ Html::tag(
+ 'span',
+ ['style' => 'font-size: 75%'],
+ sprintf('Trying to access a missing business process node "%s"', $node->getNodeName())
+ )
+ ));
+ }
+
+ if ($this->renderer->rendersSubNode()
+ && $this->renderer->getParentNode()->getChildState($node) !== $node->getState()
+ ) {
+ $this->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'class' => 'overridden-state',
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+ }
+
+ if ($node instanceof BpNode && !$renderer->isBreadcrumb()) {
+ $this->add($renderer->renderStateBadges($node->getStateSummary(), $node->countChildren()));
+ }
+
+ return parent::render();
+ }
+
+ protected function getMainNodeUrl(Node $node)
+ {
+ if ($node instanceof BpNode) {
+ return $this->makeBpUrl($node);
+ } else {
+ /** @var MonitoredNode $node */
+ return $node->getUrl();
+ }
+ }
+
+ protected function buildBaseNodeUrl(Node $node)
+ {
+ $url = $this->renderer->getBaseUrl();
+
+ $p = $url->getParams();
+ if ($node instanceof ImportedNode
+ && $this->renderer->getBusinessProcess()->getName() === $node->getBpConfig()->getName()
+ ) {
+ $p->set('node', $node->getNodeName());
+ } elseif ($this->renderer->rendersImportedNode()) {
+ $p->set('node', $node->getIdentifier());
+ } else {
+ $p->set('node', $node->getName());
+ }
+
+ if (! empty($this->path)) {
+ $p->addValues('path', $this->path);
+ }
+
+ return $url;
+ }
+
+ protected function makeBpUrl(BpNode $node)
+ {
+ return $this->buildBaseNodeUrl($node);
+ }
+
+ /**
+ * @return BaseHtmlElement
+ */
+ protected function getMainNodeLink()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+ if ($node instanceof MonitoredNode) {
+ $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getAlias());
+ } else {
+ $link = Html::tag('a', ['href' => $url], $node->getAlias());
+ }
+
+ return $link;
+ }
+
+ protected function addDetailsActions()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+
+ if ($node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tile'),
+ 'title' => mt('businessprocess', 'Show tiles for this subtree')
+ ],
+ Html::tag('i', ['class' => 'icon icon-dashboard'])
+ ))->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tree'),
+ 'title' => mt('businessprocess', 'Show this subtree as a tree')
+ ],
+ Html::tag('i', ['class' => 'icon icon-sitemap'])
+ ));
+ if ($node instanceof ImportedNode) {
+ if ($node->getBpConfig()->hasNode($node->getName())) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_next',
+ 'href' => $this->renderer->getSourceUrl($node)->getAbsoluteUrl(),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show this process as part of its original configuration'
+ )
+ ],
+ Html::tag('i', ['class' => 'icon icon-forward'])
+ ));
+ }
+ }
+
+ $url = $node->getInfoUrl();
+
+ if ($url !== null) {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'class' => 'node-info',
+ 'title' => sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ ],
+ Html::tag('i', ['class' => 'icon icon-info-circled'])
+ );
+ if (preg_match('#^http(?:s)?://#', $url)) {
+ $link->addAttributes(['target' => '_blank']);
+ }
+ $this->actions()->add($link);
+ }
+ } else {
+ // $url = $this->makeMonitoredNodeUrl($node);
+ if ($node instanceof ServiceNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ ['href' => $node->getUrl(), 'data-base-target' => '_next'],
+ Html::tag('i', ['class' => 'icon icon-service'])
+ ));
+ } elseif ($node instanceof HostNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ ['href' => $node->getUrl(), 'data-base-target' => '_next'],
+ Html::tag('i', ['class' => 'icon icon-host'])
+ ));
+ }
+ }
+ }
+
+ protected function addActionLinks()
+ {
+ $parent = $this->renderer->getParentNode();
+ if ($parent !== null) {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $parent->getBpConfig()->getName(),
+ 'node' => $parent instanceof ImportedNode
+ ? $parent->getNodeName()
+ : $parent->getName(),
+ 'unlocked' => true
+ ]);
+ } else {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $this->node->getBpConfig()->getName(),
+ 'unlocked' => true
+ ]);
+ }
+
+ if ($this->node instanceof MonitoredNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'simulation')
+ ->with('simulationnode', $this->node->getName()),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show the business impact of this node by simulating a specific state'
+ )
+ ],
+ Html::tag('i', ['class' => 'icon icon-magic'])
+ ));
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'editmonitored')
+ ->with('editmonitorednode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this monitored node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-edit'])
+ ));
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()
+ && $this->node->getBpConfig()->getName() === $this->renderer->getBusinessProcess()->getName()
+ && $this->node->getName() !== '__unbound__'
+ ) {
+ if ($this->node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'edit')
+ ->with('editnode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this business process node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-edit'])
+ ));
+
+ $addUrl = $baseUrl->with([
+ 'node' => $this->node->getName(),
+ 'action' => 'add'
+ ]);
+ $addUrl->getParams()->addValues('path', $this->path);
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $addUrl,
+ 'title' => mt('businessprocess', 'Add a new sub-node to this business process')
+ ],
+ Html::tag('i', ['class' => 'icon icon-plus'])
+ ));
+ }
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()) {
+ $params = array(
+ 'action' => 'delete',
+ 'deletenode' => $this->node->getName(),
+ );
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl->with($params),
+ 'title' => mt('businessprocess', 'Delete this node')
+ ],
+ Html::tag('i', ['class' => 'icon icon-cancel'])
+ ));
+ }
+ }
+}
diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php
new file mode 100644
index 0000000..c71a4f9
--- /dev/null
+++ b/library/Businessprocess/Renderer/TreeRenderer.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use Icinga\Module\Icingadb\Model\State;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\StateBall;
+
+class TreeRenderer extends Renderer
+{
+ /**
+ * @inheritdoc
+ */
+ public function render()
+ {
+ $bp = $this->config;
+ $htmlId = $bp->getHtmlId();
+ $tree = Html::tag(
+ 'ul',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
+ 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $this->wantsRootNodes() ? 'root' : $htmlId,
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-sortable-invert-swap' => 'true',
+ 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false',
+ 'data-csrf-token' => CsrfToken::generate()
+ ],
+ $this->renderBp($bp)
+ );
+ if ($this->wantsRootNodes()) {
+ $tree->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $tree->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $this->add($tree);
+ return parent::render();
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @return string
+ */
+ public function renderBp(BpConfig $bp)
+ {
+ $html = array();
+ if ($this->wantsRootNodes()) {
+ $nodes = $bp->getChildren();
+ } else {
+ $nodes = $this->parent->getChildren();
+ }
+
+ foreach ($nodes as $name => $node) {
+ if ($node instanceof BpNode) {
+ $html[] = $this->renderNode($bp, $node);
+ } else {
+ $html[] = $this->renderChild($bp, $this->parent, $node);
+ }
+ }
+
+ return $html;
+ }
+
+ protected function getStateClassNames(Node $node)
+ {
+ $state = strtolower($node->getStateName());
+
+ if ($node->isMissing()) {
+ return array('missing');
+ } elseif ($state === 'ok') {
+ if ($node->hasMissingChildren()) {
+ return array('ok', 'missing-children');
+ } else {
+ return array('ok');
+ }
+ } else {
+ return array('problem', $state);
+ }
+ }
+
+ /**
+ * @param Node $node
+ * @param array $path
+ * @param BpNode $parent
+ * @return BaseHtmlElement[]
+ */
+ public function getNodeIcons(Node $node, array $path = null, BpNode $parent = null)
+ {
+ $icons = [];
+ if (empty($path) && $node instanceof BpNode) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']);
+ } else {
+ $icons[] = $node->getIcon();
+ }
+ $state = strtolower($node->getStateName($parent !== null ? $parent->getChildState($node) : null));
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $icons[] = (new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]);
+ if ($node->isInDowntime()) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-plug']);
+ }
+ if ($node->isAcknowledged()) {
+ $icons[] = Html::tag('i', ['class' => 'icon icon-ok']);
+ }
+ return $icons;
+ }
+
+ public function getOverriddenState($fakeState, Node $node)
+ {
+ $overriddenState = Html::tag('div', ['class' => 'overridden-state']);
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+ $overriddenState->add(Html::tag('i', ['class' => 'icon icon-right-small']));
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName($fakeState)), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName($fakeState)
+ ),
+ 'class' => 'last'
+ ])
+ );
+
+ return $overriddenState;
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Node $node
+ * @param array $path
+ *
+ * @return string
+ */
+ public function renderNode(BpConfig $bp, Node $node, $path = array())
+ {
+ $htmlId = $this->getId($node, $path);
+ $li = Html::tag(
+ 'li',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'movable', $node->getObjectClassName()],
+ 'data-node-name' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ]
+ );
+ $attributes = $li->getAttributes();
+ $attributes->add('class', $this->getStateClassNames($node));
+ if ($node->isHandled()) {
+ $attributes->add('class', 'handled');
+ }
+ if ($node instanceof BpNode) {
+ $attributes->add('class', 'operator');
+ } else {
+ $attributes->add('class', 'node');
+ }
+
+ $div = Html::tag('div');
+ $li->add($div);
+
+ $div->add($node->getLink());
+ $div->add($this->getNodeIcons($node, $path));
+
+ $div->add(Html::tag('span', null, $node->getAlias()));
+
+ if ($node instanceof BpNode) {
+ $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
+ }
+
+ if ($node instanceof BpNode && $node->hasInfoUrl()) {
+ $div->add($this->createInfoAction($node));
+ }
+
+ $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName();
+ if (! $this->isLocked() && !$differentConfig) {
+ $div->add($this->getActionIcons($bp, $node));
+ } elseif ($differentConfig) {
+ $div->add($this->actionIcon(
+ 'forward',
+ $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
+ mt('businessprocess', 'Show this process as part of its original configuration')
+ )->addAttributes(['data-base-target' => '_next']));
+ }
+
+ $ul = Html::tag('ul', [
+ 'class' => ['bp', 'sortable'],
+ 'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false',
+ 'data-sortable-invert-swap' => 'true',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-draggable' => '.movable',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $htmlId, // Unique, so that the function below is the only deciding factor
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-csrf-token' => CsrfToken::generate(),
+ 'data-action-url' => $this->getUrl()
+ ->with([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ])
+ ->getAbsoluteUrl()
+ ]);
+ $li->add($ul);
+
+ $path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
+ foreach ($node->getChildren() as $name => $child) {
+ if ($child instanceof BpNode) {
+ $ul->add($this->renderNode($bp, $child, $path));
+ } else {
+ $ul->add($this->renderChild($bp, $node, $child, $path));
+ }
+ }
+
+ return $li;
+ }
+
+ protected function renderChild($bp, BpNode $parent, Node $node, $path = null)
+ {
+ $li = Html::tag('li', [
+ 'class' => 'movable',
+ 'id' => $this->getId($node, $path ?: []),
+ 'data-node-name' => $node->getName()
+ ]);
+
+ $li->add($this->getNodeIcons($node, $path, $parent));
+
+ $link = $node->getLink();
+ $link->getAttributes()->set('data-base-target', '_next');
+ $li->add($link);
+
+ if (($overriddenState = $parent->getChildState($node)) !== $node->getState()) {
+ $li->add($this->getOverriddenState($overriddenState, $node));
+ }
+
+ if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) {
+ $li->add($this->getActionIcons($bp, $node));
+ }
+
+ return $li;
+ }
+
+ protected function getActionIcons(BpConfig $bp, Node $node)
+ {
+ if ($node instanceof BpNode) {
+ if ($bp->getMetadata()->canModify()) {
+ return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)];
+ } else {
+ return '';
+ }
+ } else {
+ return $this->createSimulationAction($bp, $node);
+ }
+ }
+
+ protected function createEditAction(BpConfig $bp, BpNode $node)
+ {
+ return $this->actionIcon(
+ 'edit',
+ $this->getUrl()->with(array(
+ 'action' => 'edit',
+ 'editnode' => $node->getName()
+ )),
+ mt('businessprocess', 'Modify this node')
+ );
+ }
+
+ protected function createSimulationAction(BpConfig $bp, Node $node)
+ {
+ return $this->actionIcon(
+ 'magic',
+ $this->getUrl()->with(array(
+ //'config' => $bp->getName(),
+ 'action' => 'simulation',
+ 'simulationnode' => $node->getName()
+ )),
+ mt('businessprocess', 'Simulate a specific state')
+ );
+ }
+
+ protected function createInfoAction(BpNode $node)
+ {
+ $url = $node->getInfoUrl();
+ return $this->actionIcon(
+ 'help',
+ $url,
+ sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ )->addAttributes(['target' => '_blank']);
+ }
+
+ protected function actionIcon($icon, $url, $title)
+ {
+ return Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'title' => $title,
+ 'class' => 'action-link'
+ ],
+ Html::tag('i', ['class' => 'icon icon-' . $icon])
+ );
+ }
+
+ protected function renderAddNewNode($parent)
+ {
+ return $this->actionIcon(
+ 'plus',
+ $this->getUrl()
+ ->with('action', 'add')
+ ->with('node', $parent->getName()),
+ mt('businessprocess', 'Add a new business process node')
+ );
+ }
+}