diff options
Diffstat (limited to 'library/Businessprocess/Renderer')
-rw-r--r-- | library/Businessprocess/Renderer/Breadcrumb.php | 78 | ||||
-rw-r--r-- | library/Businessprocess/Renderer/Renderer.php | 398 | ||||
-rw-r--r-- | library/Businessprocess/Renderer/TileRenderer.php | 90 | ||||
-rw-r--r-- | library/Businessprocess/Renderer/TileRenderer/NodeTile.php | 357 | ||||
-rw-r--r-- | library/Businessprocess/Renderer/TreeRenderer.php | 357 |
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') + ); + } +} |