1<?php
2
3namespace Icinga\Module\Businessprocess\Renderer;
4
5use Icinga\Date\DateFormatter;
6use Icinga\Module\Businessprocess\BpConfig;
7use Icinga\Module\Businessprocess\BpNode;
8use Icinga\Module\Businessprocess\ImportedNode;
9use Icinga\Module\Businessprocess\Node;
10use Icinga\Module\Businessprocess\Web\Component\StateBall;
11use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
12use ipl\Html\BaseHtmlElement;
13use ipl\Html\Html;
14
15class TreeRenderer extends Renderer
16{
17    /**
18     * @inheritdoc
19     */
20    public function render()
21    {
22        $bp = $this->config;
23        $htmlId = $bp->getHtmlId();
24        $tree = Html::tag(
25            'ul',
26            [
27                'id'                            => $htmlId,
28                'class'                         => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
29                'data-sortable-disabled'        => $this->isLocked() ? 'true' : 'false',
30                'data-sortable-data-id-attr'    => 'id',
31                'data-sortable-direction'       => 'vertical',
32                'data-sortable-group'           => json_encode([
33                    'name'  => $this->wantsRootNodes() ? 'root' : $htmlId,
34                    'put'   => 'function:rowPutAllowed'
35                ]),
36                'data-sortable-invert-swap'     => 'true',
37                'data-is-root-config'           => $this->wantsRootNodes() ? 'true' : 'false',
38                'data-csrf-token'               => CsrfToken::generate()
39            ],
40            $this->renderBp($bp)
41        );
42        if ($this->wantsRootNodes()) {
43            $tree->getAttributes()->add(
44                'data-action-url',
45                $this->getUrl()->setParams(['config' => $bp->getName()])->getAbsoluteUrl()
46            );
47        } else {
48            $nodeName = $this->parent instanceof ImportedNode
49                ? $this->parent->getNodeName()
50                : $this->parent->getName();
51            $tree->getAttributes()
52                ->add('data-node-name', $nodeName)
53                ->add('data-action-url', $this->getUrl()
54                    ->setParams([
55                        'config'    => $this->parent->getBpConfig()->getName(),
56                        'node'      => $nodeName
57                    ])
58                    ->getAbsoluteUrl());
59        }
60
61        $this->add($tree);
62        return parent::render();
63    }
64
65    /**
66     * @param BpConfig $bp
67     * @return string
68     */
69    public function renderBp(BpConfig $bp)
70    {
71        $html = array();
72        if ($this->wantsRootNodes()) {
73            $nodes = $bp->getChildren();
74        } else {
75            $nodes = $this->parent->getChildren();
76        }
77
78        foreach ($nodes as $name => $node) {
79            if ($node instanceof BpNode) {
80                $html[] = $this->renderNode($bp, $node);
81            } else {
82                $html[] = $this->renderChild($bp, $node);
83            }
84        }
85
86        return $html;
87    }
88
89    protected function getStateClassNames(Node $node)
90    {
91        $state = strtolower($node->getStateName());
92
93        if ($node->isMissing()) {
94            return array('missing');
95        } elseif ($state === 'ok') {
96            if ($node->hasMissingChildren()) {
97                return array('ok', 'missing-children');
98            } else {
99                return array('ok');
100            }
101        } else {
102            return array('problem', $state);
103        }
104    }
105
106    /**
107     * @param Node $node
108     * @param array $path
109     * @return BaseHtmlElement[]
110     */
111    public function getNodeIcons(Node $node, array $path = null)
112    {
113        $icons = [];
114        if (empty($path) && $node instanceof BpNode) {
115            $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']);
116        } else {
117            $icons[] = $node->getIcon();
118        }
119        $icons[] = (new StateBall(strtolower($node->getStateName())))->addAttributes([
120            'title' => sprintf(
121                '%s %s',
122                $node->getStateName(),
123                DateFormatter::timeSince($node->getLastStateChange())
124            )
125        ]);
126        if ($node->isInDowntime()) {
127            $icons[] = Html::tag('i', ['class' => 'icon icon-moon']);
128        }
129        if ($node->isAcknowledged()) {
130            $icons[] = Html::tag('i', ['class' => 'icon icon-ok']);
131        }
132        return $icons;
133    }
134
135    /**
136     * @param BpConfig $bp
137     * @param Node $node
138     * @param array $path
139     *
140     * @return string
141     */
142    public function renderNode(BpConfig $bp, Node $node, $path = array())
143    {
144        $htmlId = $this->getId($node, $path);
145        $li = Html::tag(
146            'li',
147            [
148                'id'                => $htmlId,
149                'class'             => ['bp', 'movable', $node->getObjectClassName()],
150                'data-node-name'    => $node instanceof ImportedNode
151                    ? $node->getNodeName()
152                    : $node->getName()
153            ]
154        );
155        $attributes = $li->getAttributes();
156        $attributes->add('class', $this->getStateClassNames($node));
157        if ($node->isHandled()) {
158            $attributes->add('class', 'handled');
159        }
160        if ($node instanceof BpNode) {
161            $attributes->add('class', 'operator');
162        } else {
163            $attributes->add('class', 'node');
164        }
165
166        $div = Html::tag('div');
167        $li->add($div);
168
169        $div->add($node->getLink());
170        $div->add($this->getNodeIcons($node, $path));
171
172        $div->add(Html::tag('span', null, $node->getAlias()));
173
174        if ($node instanceof BpNode) {
175            $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
176        }
177
178        if ($node instanceof BpNode && $node->hasInfoUrl()) {
179            $div->add($this->createInfoAction($node));
180        }
181
182        $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName();
183        if (! $this->isLocked() && !$differentConfig) {
184            $div->add($this->getActionIcons($bp, $node));
185        } elseif ($differentConfig) {
186            $div->add($this->actionIcon(
187                'forward',
188                $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
189                mt('businessprocess', 'Show this process as part of its original configuration')
190            )->addAttributes(['data-base-target' => '_next']));
191        }
192
193        $ul = Html::tag('ul', [
194            'class'                         => ['bp', 'sortable'],
195            'data-sortable-disabled'        => ($this->isLocked() || $differentConfig) ? 'true' : 'false',
196            'data-sortable-invert-swap'     => 'true',
197            'data-sortable-data-id-attr'    => 'id',
198            'data-sortable-draggable'       => '.movable',
199            'data-sortable-direction'       => 'vertical',
200            'data-sortable-group'           => json_encode([
201                'name'  => $htmlId, // Unique, so that the function below is the only deciding factor
202                'put'   => 'function:rowPutAllowed'
203            ]),
204            'data-csrf-token'               => CsrfToken::generate(),
205            'data-action-url'               => $this->getUrl()
206                ->setParams([
207                    'config'    => $node->getBpConfig()->getName(),
208                    'node'      => $node instanceof ImportedNode
209                        ? $node->getNodeName()
210                        : $node->getName()
211                ])
212                ->getAbsoluteUrl()
213        ]);
214        $li->add($ul);
215
216        $path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
217        foreach ($node->getChildren() as $name => $child) {
218            if ($child instanceof BpNode) {
219                $ul->add($this->renderNode($bp, $child, $path));
220            } else {
221                $ul->add($this->renderChild($bp, $child, $path));
222            }
223        }
224
225        return $li;
226    }
227
228    protected function renderChild($bp, Node $node, $path = null)
229    {
230        $li = Html::tag('li', [
231            'class'             => 'movable',
232            'id'                => $this->getId($node, $path ?: []),
233            'data-node-name'    => $node->getName()
234        ]);
235
236        $li->add($this->getNodeIcons($node, $path));
237
238        $link = $node->getLink();
239        $link->getAttributes()->set('data-base-target', '_next');
240        $li->add($link);
241
242        if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) {
243            $li->add($this->getActionIcons($bp, $node));
244        }
245
246        return $li;
247    }
248
249    protected function getActionIcons(BpConfig $bp, Node $node)
250    {
251        if ($node instanceof BpNode) {
252            if ($bp->getMetadata()->canModify()) {
253                return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)];
254            } else {
255                return '';
256            }
257        } else {
258            return $this->createSimulationAction($bp, $node);
259        }
260    }
261
262    protected function createEditAction(BpConfig $bp, BpNode $node)
263    {
264        return $this->actionIcon(
265            'edit',
266            $this->getUrl()->with(array(
267                'action'   => 'edit',
268                'editnode' => $node->getName()
269            )),
270            mt('businessprocess', 'Modify this node')
271        );
272    }
273
274    protected function createSimulationAction(BpConfig $bp, Node $node)
275    {
276        return $this->actionIcon(
277            'magic',
278            $this->getUrl()->with(array(
279                //'config' => $bp->getName(),
280                'action' => 'simulation',
281                'simulationnode' => $node->getName()
282            )),
283            mt('businessprocess', 'Simulate a specific state')
284        );
285    }
286
287    protected function createInfoAction(BpNode $node)
288    {
289        $url = $node->getInfoUrl();
290        return $this->actionIcon(
291            'help',
292            $url,
293            sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
294        )->addAttributes(['target' => '_blank']);
295    }
296
297    protected function actionIcon($icon, $url, $title)
298    {
299        return Html::tag(
300            'a',
301            [
302                'href'  => $url,
303                'title' => $title,
304                'class' => 'action-link'
305            ],
306            Html::tag('i', ['class' => 'icon icon-' . $icon])
307        );
308    }
309
310    protected function renderAddNewNode($parent)
311    {
312        return $this->actionIcon(
313            'plus',
314            $this->getUrl()
315                ->with('action', 'add')
316                ->with('node', $parent->getName()),
317            mt('businessprocess', 'Add a new business process node')
318        );
319    }
320}
321