Support custom ordering of nodes (#384)

resolves #252
closes #297 

requires https://github.com/Icinga/ipl-web/pull/173
This commit is contained in:
Johannes Meyer 2023-08-03 15:22:40 +02:00 committed by GitHub
commit be1f56ba08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 586 additions and 487 deletions

View file

@ -26,10 +26,13 @@ use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget\Tabextension\DashboardAction;
use Icinga\Web\Widget\Tabextension\OutputFormat;
use ipl\Html\Form;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\TemplateString;
use ipl\Html\Text;
use ipl\Web\Control\SortControl;
use ipl\Web\Widget\Link;
class ProcessController extends Controller
@ -117,7 +120,7 @@ class ProcessController extends Controller
$this->tabs()->extend(new OutputFormat());
$this->content()->add($this->showHints($bp));
$this->content()->add($this->showHints($bp, $renderer));
$this->content()->add($this->showWarnings($bp));
$this->content()->add($this->showErrors($bp));
$this->content()->add($renderer);
@ -125,6 +128,50 @@ class ProcessController extends Controller
$this->setDynamicAutorefresh();
}
/**
* Create a sort control and apply its sort specification to the given renderer
*
* @param Renderer $renderer
* @param BpConfig $config
*
* @return SortControl
*/
protected function createBpSortControl(Renderer $renderer, BpConfig $config): SortControl
{
$defaultSort = $this->session()->get('sort.default', $renderer->getDefaultSort());
$options = [
'display_name asc' => $this->translate('Name'),
'state desc' => $this->translate('State')
];
if ($config->getMetadata()->isManuallyOrdered()) {
$options['manual asc'] = $this->translate('Manual');
} elseif ($defaultSort === 'manual desc') {
$defaultSort = $renderer->getDefaultSort();
}
$sortControl = SortControl::create($options)
->setDefault($defaultSort)
->setMethod('POST')
->setAttribute('name', 'bp-sort-control')
->on(Form::ON_SUCCESS, function (SortControl $sortControl) use ($renderer) {
$sort = $sortControl->getSort();
if ($sort === $renderer->getDefaultSort()) {
$this->session()->delete('sort.default');
$url = Url::fromRequest()->without($sortControl->getSortParam());
} else {
$this->session()->set('sort.default', $sort);
$url = Url::fromRequest()->with($sortControl->getSortParam(), $sort);
}
$this->redirectNow($url);
})->handleRequest($this->getServerRequest());
$renderer->setSort($sortControl->getSort());
$this->params->shift($sortControl->getSortParam());
return $sortControl;
}
protected function prepareControls($bp, $renderer)
{
$controls = $this->controls();
@ -153,6 +200,8 @@ class ProcessController extends Controller
new RenderedProcessActionBar($bp, $renderer, $this->Auth(), $this->url())
);
}
$controls->addHtml($this->createBpSortControl($renderer, $bp));
}
protected function getNode(BpConfig $bp)
@ -263,7 +312,21 @@ class ProcessController extends Controller
->setSimulation(Simulation::fromSession($this->session()))
->handleRequest();
} elseif ($action === 'move') {
$successUrl = $this->url()->without(['action', 'movenode']);
if ($this->params->get('mode') === 'tree') {
// If the user moves a node from a subtree, the `node` param exists
$successUrl->getParams()->remove('node');
}
if ($this->session()->get('sort.default')) {
// If there's a default sort specification in the session, it can only be `display_name desc`,
// as otherwise the user wouldn't be able to trigger this action. So it's safe to just define
// descending manual order now.
$successUrl->getParams()->add(SortControl::DEFAULT_SORT_PARAM, 'manual desc');
}
$form = $this->loadForm('MoveNode')
->setSuccessUrl($successUrl)
->setProcess($bp)
->setParentNode($node)
->setSession($this->session())
@ -321,7 +384,7 @@ class ProcessController extends Controller
}
}
protected function showHints(BpConfig $bp)
protected function showHints(BpConfig $bp, Renderer $renderer)
{
$ul = Html::tag('ul', ['class' => 'error']);
$this->prepareMissingNodeLinks($ul);
@ -362,6 +425,20 @@ class ProcessController extends Controller
$ul->add($li);
}
if (! $renderer->isLocked() && $renderer->appliesCustomSorting()) {
$ul->addHtml(Html::tag('li', null, [
Text::create($this->translate('Drag&Drop disabled. Custom sort order applied.')),
(new Form())
->setAttribute('class', 'inline')
->addElement('submitButton', SortControl::DEFAULT_SORT_PARAM, [
'label' => $this->translate('Reset to default'),
'value' => $renderer->getDefaultSort(),
'class' => 'link-button'
])
->addElement('hidden', 'uid', ['value' => 'bp-sort-control'])
])->setSeparator(' '));
}
if (! $ul->isEmpty()) {
return $ul;
} else {
@ -510,7 +587,7 @@ class ProcessController extends Controller
->setParams($this->getRequest()->getUrl()->getParams());
$this->content()->add(
$this->loadForm('bpConfig')
->setProcessConfig($bp)
->setProcess($bp)
->setStorage($this->storage())
->setSuccessUrl($url)
->handleRequest()
@ -647,7 +724,7 @@ class ProcessController extends Controller
if (isset($node['since'])) {
$data[] = DateFormatter::formatDateTime($node['since']);
}
if (isset($node['in_downtime'])) {
$data[] = $node['in_downtime'];
}

View file

@ -4,30 +4,18 @@ namespace Icinga\Module\Businessprocess\Forms;
use Exception;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Common\Sort;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Storage\Storage;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
class AddNodeForm extends QuickForm
class AddNodeForm extends BpConfigBaseForm
{
use EnumList;
/** @var MonitoringBackend|IcingaDbConnection*/
protected $backend;
/** @var Storage */
protected $storage;
/** @var BpConfig */
protected $bp;
use Sort;
/** @var BpNode */
protected $parent;
@ -36,9 +24,6 @@ class AddNodeForm extends QuickForm
protected $processList = array();
/** @var SessionNamespace */
protected $session;
public function setup()
{
$view = $this->getView();
@ -119,8 +104,8 @@ class AddNodeForm extends QuickForm
));
$display = 1;
if ($this->bp->getMetadata()->isManuallyOrdered() && !$this->bp->isEmpty()) {
$rootNodes = $this->bp->getRootNodes();
if ($this->bp->getMetadata()->isManuallyOrdered() && ! $this->bp->isEmpty()) {
$rootNodes = self::applyManualSorting($this->bp->getRootNodes());
$display = end($rootNodes)->getDisplay() + 1;
}
$this->addElement('select', 'display', array(
@ -389,37 +374,6 @@ class AddNodeForm extends QuickForm
}
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param Storage $storage
* @return $this
*/
public function setStorage(Storage $storage)
{
$this->storage = $storage;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param BpNode|null $node
* @return $this
@ -438,16 +392,6 @@ class AddNodeForm extends QuickForm
return $this->parent !== null;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
protected function hasProcesses()
{
return count($this->enumProcesses()) > 0;
@ -492,9 +436,6 @@ class AddNodeForm extends QuickForm
}
}
if (! $this->bp->getMetadata()->isManuallyOrdered()) {
natcasesort($list);
}
return $list;
}

View file

@ -116,12 +116,12 @@ class BpConfigForm extends BpConfigBaseForm
),
));
if ($this->config === null) {
if ($this->bp === null) {
$this->setSubmitLabel(
$this->translate('Add')
);
} else {
$config = $this->config;
$config = $this->bp;
$meta = $config->getMetadata();
foreach ($meta->getProperties() as $k => $v) {
@ -156,13 +156,13 @@ class BpConfigForm extends BpConfigBaseForm
$name = $this->getValue('name');
if ($this->shouldBeDeleted()) {
if ($this->config->isReferenced()) {
if ($this->bp->isReferenced()) {
$this->addError(sprintf(
$this->translate('Process "%s" cannot be deleted as it has been referenced in other processes'),
$name
));
} else {
$this->config->clearAppliedChanges();
$this->bp->clearAppliedChanges();
$this->storage->deleteProcess($name);
$this->setSuccessUrl('businessprocess');
$this->redirectOnSuccess(sprintf('Process %s has been deleted', $name));
@ -174,7 +174,7 @@ class BpConfigForm extends BpConfigBaseForm
{
$name = $this->getValue('name');
if ($this->config === null) {
if ($this->bp === null) {
if ($this->storage->hasProcess($name)) {
$this->addError(sprintf(
$this->translate('A process named "%s" already exists'),
@ -199,7 +199,7 @@ class BpConfigForm extends BpConfigBaseForm
);
$this->setSuccessMessage(sprintf('Process %s has been created', $name));
} else {
$config = $this->config;
$config = $this->bp;
$this->setSuccessMessage(sprintf('Process %s has been stored', $name));
}
$meta = $config->getMetadata();

View file

@ -10,8 +10,6 @@ use Icinga\Web\Notification;
class BpUploadForm extends BpConfigBaseForm
{
protected $backend;
protected $node;
protected $objectList = array();

View file

@ -3,31 +3,18 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
class DeleteNodeForm extends QuickForm
class DeleteNodeForm extends BpConfigBaseForm
{
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var Node */
protected $node;
/** @var BpNode */
protected $parentNode;
/** @var SessionNamespace */
protected $session;
public function setup()
{
$node = $this->node;
@ -80,27 +67,6 @@ class DeleteNodeForm extends QuickForm
));
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param Node $node
* @return $this
@ -121,16 +87,6 @@ class DeleteNodeForm extends QuickForm
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);

View file

@ -3,26 +3,16 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
class EditNodeForm extends QuickForm
class EditNodeForm extends BpConfigBaseForm
{
use EnumList;
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var Node */
protected $node;
@ -37,9 +27,6 @@ class EditNodeForm extends QuickForm
protected $host;
/** @var SessionNamespace */
protected $session;
public function setup()
{
$this->host = substr($this->getNode()->getName(), 0, strpos($this->getNode()->getName(), ';'));
@ -283,27 +270,6 @@ class EditNodeForm extends QuickForm
));
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param BpNode|null $node
* @return $this
@ -322,16 +288,6 @@ class EditNodeForm extends QuickForm
return $this->parent !== null;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
protected function hasProcesses()
{
return count($this->enumProcesses()) > 0;
@ -354,9 +310,6 @@ class EditNodeForm extends QuickForm
}
}
if (! $this->bp->getMetadata()->isManuallyOrdered()) {
natcasesort($list);
}
return $list;
}

View file

@ -171,7 +171,11 @@ class MoveNodeForm extends QuickForm
$this->notifySuccess($this->getSuccessMessage($this->translate('Node order updated')));
$response = $this->getRequest()->getResponse()
->setHeader('X-Icinga-Container', 'ignore');
->setHeader('X-Icinga-Container', 'ignore')
->setHeader('X-Icinga-Extra-Updates', implode(';', [
$this->getRequest()->getHeader('X-Icinga-Container'),
$this->getSuccessUrl()->getAbsoluteUrl()
]));
Session::getSession()->write();
$response->sendResponse();

View file

@ -3,29 +3,16 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Web\Notification;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
class ProcessForm extends QuickForm
class ProcessForm extends BpConfigBaseForm
{
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var BpNode */
protected $node;
/** @var SessionNamespace */
protected $session;
public function setup()
{
if ($this->node !== null) {
@ -94,27 +81,6 @@ class ProcessForm extends QuickForm
}
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param BpNode $node
* @return $this
@ -125,16 +91,6 @@ class ProcessForm extends QuickForm
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);

View file

@ -4,9 +4,9 @@ namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\MonitoredNode;
use Icinga\Module\Businessprocess\Simulation;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
class SimulationForm extends QuickForm
class SimulationForm extends BpConfigBaseForm
{
/** @var MonitoredNode */
protected $node;

View file

@ -432,33 +432,12 @@ class BpConfig
*/
public function getRootNodes()
{
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($this->root_nodes, function (BpNode $a, BpNode $b) {
$a = $a->getDisplay();
$b = $b->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE);
}
return $this->root_nodes;
}
public function listRootNodes()
{
$names = array_keys($this->root_nodes);
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($names, function ($a, $b) {
$a = $this->root_nodes[$a]->getDisplay();
$b = $this->root_nodes[$b]->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
natcasesort($names);
}
return $names;
return array_keys($this->root_nodes);
}
public function getNodes()
@ -826,16 +805,6 @@ class BpConfig
$nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node);
}
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($nodes, function ($a, $b) {
$a = $this->nodes[$a]->getDisplay();
$b = $this->nodes[$b]->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
natcasesort($nodes);
}
return $nodes;
}

View file

@ -135,7 +135,6 @@ class BpNode extends Node
$this->children[$name] = $node;
$this->childNames[] = $name;
$this->reorderChildren();
$node->addParent($this);
return $this;
}
@ -549,7 +548,6 @@ class BpNode extends Node
{
$this->childNames = $names;
$this->children = null;
$this->reorderChildren();
return $this;
}
@ -568,7 +566,6 @@ class BpNode extends Node
{
if ($this->children === null) {
$this->children = [];
$this->reorderChildren();
foreach ($this->getChildNames() as $name) {
$this->children[$name] = $this->getBpConfig()->getNode($name);
$this->children[$name]->addParent($this);
@ -578,29 +575,6 @@ class BpNode extends Node
return $this->children;
}
/**
* Reorder this node's children, in case manual order is not applied
*/
protected function reorderChildren()
{
if ($this->getBpConfig()->getMetadata()->isManuallyOrdered()) {
return;
}
$childNames = $this->getChildNames();
natcasesort($childNames);
$this->childNames = array_values($childNames);
if (! empty($this->children)) {
$children = [];
foreach ($this->childNames as $name) {
$children[$name] = $this->children[$name];
}
$this->children = $children;
}
}
/**
* return BpNode[]
*/

View file

@ -0,0 +1,154 @@
<?php
// Icinga Business Process Modelling | (c) 2023 Icinga GmbH | GPLv2
namespace Icinga\Module\Businessprocess\Common;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Node;
use InvalidArgumentException;
use ipl\Stdlib\Str;
trait Sort
{
/** @var string Current sort specification */
protected $sort;
/** @var callable Actual sorting function */
protected $sortFn;
/**
* Get the sort specification
*
* @return ?string
*/
public function getSort(): ?string
{
return $this->sort;
}
/**
* Set the sort specification
*
* @param string $sort
*
* @return $this
*
* @throws InvalidArgumentException When sorting according to the specified specification is not possible
*/
public function setSort(string $sort): self
{
list($sortBy, $direction) = Str::symmetricSplit($sort, ' ', 2, 'asc');
switch ($sortBy) {
case 'manual':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
$firstNode = reset($nodes);
if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
$nodes = self::applyManualSorting($nodes);
}
// Child nodes don't need to be ordered in this case, their implicit order is significant
};
} else {
$this->sortFn = function (array &$nodes) {
$firstNode = reset($nodes);
if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
uasort($nodes, function (BpNode $a, BpNode $b) {
return $b->getDisplay() <=> $a->getDisplay();
});
} else {
$nodes = array_reverse($nodes);
}
};
}
break;
case 'display_name':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return strnatcasecmp(
$a->getAlias() ?? $a->getName(),
$b->getAlias() ?? $b->getName()
);
});
};
} else {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return strnatcasecmp(
$b->getAlias() ?? $b->getName(),
$a->getAlias() ?? $a->getName()
);
});
};
}
break;
case 'state':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return $a->getSortingState() <=> $b->getSortingState();
});
};
} else {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return $b->getSortingState() <=> $a->getSortingState();
});
};
}
break;
default:
throw new InvalidArgumentException(sprintf(
"Can't sort by %s. It's only possible to sort by manual order, display_name or state",
$sortBy
));
}
$this->sort = $sort;
return $this;
}
/**
* Sort the given nodes as specified by {@see setSort()}
*
* If {@see setSort()} has not been called yet, the default sort specification is used
*
* @param array $nodes
*
* @return array
*/
public function sort(array $nodes): array
{
if (empty($nodes)) {
return $nodes;
}
if ($this->sortFn !== null) {
call_user_func_array($this->sortFn, [&$nodes]);
}
return $nodes;
}
/**
* Apply manual sort order on the given process nodes
*
* @param array $bpNodes
*
* @return array
*/
public static function applyManualSorting(array $bpNodes): array
{
uasort($bpNodes, function (BpNode $a, BpNode $b) {
return $a->getDisplay() <=> $b->getDisplay();
});
return $bpNodes;
}
}

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeApplyManualOrderAction extends NodeAction
{
use Sort;
public function appliesTo(BpConfig $config)
{
return $config->getMetadata()->get('ManualOrder') !== 'yes';
@ -20,7 +23,10 @@ class NodeApplyManualOrderAction extends NodeAction
}
if ($node->hasChildren()) {
$node->setChildNames($node->getChildNames());
$node->setChildNames(array_keys(
$this->setSort('display_name asc')
->sort($node->getChildren())
));
}
}

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeCopyAction extends NodeAction
{
use Sort;
/**
* @param BpConfig $config
* @return bool
@ -31,9 +34,15 @@ class NodeCopyAction extends NodeAction
public function applyTo(BpConfig $config)
{
$name = $this->getNodeName();
$rootNodes = $config->getRootNodes();
$display = 1;
if ($config->getMetadata()->isManuallyOrdered()) {
$rootNodes = self::applyManualSorting($config->getRootNodes());
$display = end($rootNodes)->getDisplay() + 1;
}
$config->addRootNode($name)
->getBpNode($name)
->setDisplay(end($rootNodes)->getDisplay() + 1);
->setDisplay($display);
}
}

View file

@ -4,9 +4,12 @@ namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeMoveAction extends NodeAction
{
use Sort;
/**
* @var string
*/
@ -87,16 +90,28 @@ class NodeMoveAction extends NodeAction
$nodes = $parent->getChildNames();
if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
$this->error('Node "%s" not found at position %d', $name, $this->from);
$reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
$this->error('Node "%s" not found at position %d', $name, $this->from);
} else {
$this->from = array_search($reversedNodes[$this->from], $nodes, true);
$this->to = array_search($reversedNodes[$this->to], $nodes, true);
}
}
} else {
if (! $config->hasRootNode($name)) {
$this->error('Toplevel process "%s" not found', $name);
}
$nodes = $config->listRootNodes();
$nodes = array_keys(self::applyManualSorting($config->getRootNodes()));
if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
$this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
$reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
$this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
} else {
$this->from = array_search($reversedNodes[$this->from], $nodes, true);
$this->to = array_search($reversedNodes[$this->to], $nodes, true);
}
}
}
@ -144,7 +159,7 @@ class NodeMoveAction extends NodeAction
if ($this->parent !== null) {
$nodes = $config->getBpNode($this->parent)->getChildren();
} else {
$nodes = $config->getRootNodes();
$nodes = self::applyManualSorting($config->getRootNodes());
}
$node = $nodes[$name];
@ -162,7 +177,7 @@ class NodeMoveAction extends NodeAction
if ($this->newParent !== null) {
$newNodes = $config->getBpNode($this->newParent)->getChildren();
} else {
$newNodes = $config->getRootNodes();
$newNodes = self::applyManualSorting($config->getRootNodes());
}
$newNodes = array_merge(

View file

@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Renderer;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\MonitoredNode;
use Icinga\Module\Businessprocess\Node;
@ -12,10 +13,13 @@ use Icinga\Module\Businessprocess\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Stdlib\Str;
use ipl\Web\Widget\StateBadge;
abstract class Renderer extends HtmlDocument
{
use Sort;
/** @var BpConfig */
protected $config;
@ -120,6 +124,33 @@ abstract class Renderer extends HtmlDocument
}
}
/**
* Get the default sort specification
*
* @return string
*/
public function getDefaultSort(): string
{
if ($this->config->getMetadata()->isManuallyOrdered()) {
return 'manual asc';
}
return 'display_name asc';
}
/**
* Get whether a custom sort order is applied
*
* @return bool
*/
public function appliesCustomSorting(): bool
{
list($sortBy, $_) = Str::symmetricSplit($this->getSort(), ' ', 2);
list($defaultSortBy, $_) = Str::symmetricSplit($this->getDefaultSort(), ' ', 2);
return $sortBy !== $defaultSortBy;
}
/**
* @return int
*/

View file

@ -17,7 +17,9 @@ class TileRenderer extends Renderer
[
'class' => ['sortable', 'tiles', $this->howMany()],
'data-base-target' => '_self',
'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
? 'true'
: 'false',
'data-sortable-data-id-attr' => 'id',
'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists
'data-csrf-token' => CsrfToken::generate()
@ -43,10 +45,8 @@ class TileRenderer extends Renderer
->getAbsoluteUrl());
}
$nodes = $this->getChildNodes();
$path = $this->getCurrentPath();
foreach ($nodes as $name => $node) {
foreach ($this->sort($this->getChildNodes()) as $name => $node) {
$this->add(new NodeTile($this, $node, $path));
}

View file

@ -29,7 +29,9 @@ class TreeRenderer extends Renderer
[
'id' => $htmlId,
'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
? 'true'
: 'false',
'data-sortable-data-id-attr' => 'id',
'data-sortable-direction' => 'vertical',
'data-sortable-group' => json_encode([
@ -69,18 +71,18 @@ class TreeRenderer extends Renderer
/**
* @param BpConfig $bp
* @return string
* @return array
*/
public function renderBp(BpConfig $bp)
{
$html = array();
$html = [];
if ($this->wantsRootNodes()) {
$nodes = $bp->getChildren();
$nodes = $bp->getRootNodes();
} else {
$nodes = $this->parent->getChildren();
}
foreach ($nodes as $name => $node) {
foreach ($this->sort($nodes) as $name => $node) {
if ($node instanceof BpNode) {
$html[] = $this->renderNode($bp, $node);
} else {
@ -238,7 +240,9 @@ class TreeRenderer extends Renderer
$ul = Html::tag('ul', [
'class' => ['bp', 'sortable'],
'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false',
'data-sortable-disabled' => ($this->isLocked() || $differentConfig || $this->appliesCustomSorting())
? 'true'
: 'false',
'data-sortable-invert-swap' => 'true',
'data-sortable-data-id-attr' => 'id',
'data-sortable-draggable' => '.movable',
@ -259,7 +263,7 @@ class TreeRenderer extends Renderer
]);
$path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
foreach ($node->getChildren() as $name => $child) {
foreach ($this->sort($node->getChildren()) as $name => $child) {
if ($child instanceof BpNode) {
$ul->add($this->renderNode($bp, $child, $path));
} else {

View file

@ -73,7 +73,6 @@ class LegacyStorage extends Storage
$files[$name] = $meta->getExtendedTitle();
}
natcasesort($files);
return $files;
}
@ -93,7 +92,6 @@ class LegacyStorage extends Storage
$files[$name] = $name;
}
natcasesort($files);
return $files;
}

View file

@ -12,12 +12,12 @@ use Icinga\Module\Businessprocess\Web\Component\Controls;
use Icinga\Module\Businessprocess\Web\Component\Content;
use Icinga\Module\Businessprocess\Web\Component\Tabs;
use Icinga\Module\Businessprocess\Web\Form\FormLoader;
use Icinga\Web\Controller as ModuleController;
use Icinga\Web\Notification;
use Icinga\Web\View;
use ipl\Html\Html;
use ipl\Web\Compat\CompatController;
class Controller extends ModuleController
class Controller extends CompatController
{
/** @var View */
public $view;
@ -173,14 +173,6 @@ class Controller extends ModuleController
return $this;
}
protected function setTitle($title)
{
$args = func_get_args();
array_shift($args);
$this->view->title = vsprintf($title, $args);
return $this;
}
protected function addTitle($title)
{
$args = func_get_args();

View file

@ -5,16 +5,25 @@ namespace Icinga\Module\Businessprocess\Web\Form;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Authentication\Auth;
use Icinga\Module\Businessprocess\Storage\LegacyStorage;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Storage\Storage;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
abstract class BpConfigBaseForm extends QuickForm
{
/** @var LegacyStorage */
/** @var Storage */
protected $storage;
/** @var BpConfig */
protected $config;
protected $bp;
/** @var MonitoringBackend|IcingaDbConnection*/
protected $backend;
/** @var SessionNamespace */
protected $session;
protected function listAvailableBackends()
{
@ -28,15 +37,60 @@ abstract class BpConfigBaseForm extends QuickForm
return $keys;
}
public function setStorage(LegacyStorage $storage)
/**
* Set the storage to use
*
* @param Storage $storage
*
* @return $this
*/
public function setStorage(Storage $storage): self
{
$this->storage = $storage;
return $this;
}
public function setProcessConfig(BpConfig $config)
/**
* Set the config to use
*
* @param BpConfig $config
*
* @return $this
*/
public function setProcess(BpConfig $config): self
{
$this->config = $config;
$this->bp = $config;
$this->setBackend($config->getBackend());
return $this;
}
/**
* Set the backend to use
*
* @param MonitoringBackend|IcingaDbConnection $backend
*
* @return $this
*/
public function setBackend($backend): self
{
$this->backend = $backend;
return $this;
}
/**
* Set the session namespace to use
*
* @param SessionNamespace $session
*
* @return $this
*/
public function setSession(SessionNamespace $session): self
{
$this->session = $session;
return $this;
}
@ -69,4 +123,13 @@ abstract class BpConfigBaseForm extends QuickForm
return true;
}
protected function setPreferredDecorators()
{
parent::setPreferredDecorators();
$this->setAttrib('class', $this->getAttrib('class') . ' bp-form');
return $this;
}
}

View file

@ -7,6 +7,7 @@ a:focus {
}
.action-bar {
float: left;
display: flex;
align-items: center;
font-size: 1.3em;
@ -76,6 +77,10 @@ a:focus {
}
}
.controls .sort-control {
float: right;
}
form a {
color: @icinga-blue;
}
@ -742,7 +747,8 @@ ul.error, ul.warning {
padding: 0.3em 0.8em;
}
li a {
li a,
li .link-button {
color: inherit;
text-decoration: underline;
@ -790,177 +796,178 @@ table.sourcecode {
left: -100%;
}
form input[type=file] {
padding-right: 1em;
}
form input[type=submit]:first-of-type {
border-width: 2px;
}
form p.description {
padding: 1em 1em;
margin: 0;
font-style: italic;
width: 100%;
}
form ul.form-errors {
margin-bottom: 0.5em;
ul.errors li {
background: @color-critical;
font-weight: bold;
padding: 0.5em 1em;
color: @text-color-on-icinga-blue;
}
}
input[type=text], input[type=password], input[type=file], textarea, select {
max-width: 36em;
min-width: 20em;
width: 100%;
}
label {
line-height: 2em;
}
form dl {
margin: 0;
padding: 0;
}
select option {
padding-left: 0.5em;
}
form dt label {
width: auto;
font-weight: normal;
font-size: inherit;
&.required {
&::after {
content: '*'
}
form.bp-form {
input[type=file] {
padding-right: 1em;
}
&:hover {
text-decoration: underline;
cursor: pointer;
input[type=submit]:first-of-type {
border-width: 2px;
}
}
#stateOverrides-element {
display: inline-table;
table-layout: fixed;
border-spacing: .5em;
padding: 0;
label {
display: table-row;
span, select {
display: table-cell;
}
span {
width: 10em;
}
select {
width: 26em;
}
}
}
form fieldset {
min-width: 36em;
}
form dd input.related-action[type='submit'] {
display: none;
}
form dd.active li.active input.related-action[type='submit'] {
display: inline-block;
}
form dd.active {
p.description {
color: inherit;
font-style: normal;
}
}
form dd {
padding: 0.3em 0.5em;
margin: 0;
}
form dt {
padding: 0.5em 0.5em;
margin: 0;
}
form dt.active, form dd.active {
background-color: @tr-active-color;
}
form dt {
display: inline-block;
vertical-align: top;
min-width: 12em;
min-height: 2.5em;
width: 30%;
&.errors label {
color: @color-critical;
}
}
form .errors label {
color: @color-critical;
}
form dd {
display: inline-block;
width: 63%;
min-height: 2.5em;
vertical-align: top;
margin: 0;
&.errors {
input[type=text], select {
border-color: @color-critical;
}
}
&.full-width {
padding: 0.5em;
padding: 1em 1em;
margin: 0;
font-style: italic;
width: 100%;
}
}
form dd:after {
display: block;
content: '';
}
ul.form-errors {
margin-bottom: 0.5em;
form textarea {
height: auto;
}
form dd ul.errors {
list-style-type: none;
padding-left: 0.3em;
li {
color: @color-critical;
padding: 0.3em;
ul.errors li {
background: @color-critical;
font-weight: bold;
padding: 0.5em 1em;
color: @text-color-on-icinga-blue;
}
}
}
form {
input[type=text], input[type=password], input[type=file], textarea, select {
max-width: 36em;
min-width: 20em;
width: 100%;
}
label {
line-height: 2em;
}
dl {
margin: 0;
padding: 0;
}
select option {
padding-left: 0.5em;
}
dt label {
width: auto;
font-weight: normal;
font-size: inherit;
&.required {
&::after {
content: '*'
}
}
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
#stateOverrides-element {
display: inline-table;
table-layout: fixed;
border-spacing: .5em;
padding: 0;
label {
display: table-row;
span, select {
display: table-cell;
}
span {
width: 10em;
}
select {
width: 26em;
}
}
}
fieldset {
min-width: 36em;
}
dd input.related-action[type='submit'] {
display: none;
}
dd.active li.active input.related-action[type='submit'] {
display: inline-block;
}
dd.active {
p.description {
color: inherit;
font-style: normal;
}
}
dd {
padding: 0.3em 0.5em;
margin: 0;
}
dt {
padding: 0.5em 0.5em;
margin: 0;
}
dt.active, dd.active {
background-color: @tr-active-color;
}
dt {
display: inline-block;
vertical-align: top;
min-width: 12em;
min-height: 2.5em;
width: 30%;
&.errors label {
color: @color-critical;
}
}
.errors label {
color: @color-critical;
}
dd {
display: inline-block;
width: 63%;
min-height: 2.5em;
vertical-align: top;
margin: 0;
&.errors {
input[type=text], select {
border-color: @color-critical;
}
}
&.full-width {
padding: 0.5em;
width: 100%;
}
}
dd:after {
display: block;
content: '';
}
textarea {
height: auto;
}
dd ul.errors {
list-style-type: none;
padding-left: 0.3em;
li {
color: @color-critical;
padding: 0.3em;
}
}
#_FAKE_SUBMIT {
position: absolute;
left: -100%;

View file

@ -122,11 +122,7 @@
].join('&');
var $container = $source.closest('.container');
var req = icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
req.always(function() {
icinga.loader.loadUrl(
$container.data('icingaUrl'), $container, undefined, undefined, undefined, true);
});
icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
}
},
@ -159,11 +155,7 @@
].join('&');
var $container = $target.closest('.container');
var req = icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
req.always(function() {
icinga.loader.loadUrl(
$container.data('icingaUrl'), $container, undefined, undefined, undefined, true);
});
icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
event.stopPropagation();
}
},