diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index ef525c6..75a1b86 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -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']; } diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 448250c..bab26bb 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -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; } diff --git a/application/forms/BpConfigForm.php b/application/forms/BpConfigForm.php index 2a65b45..8a0bc95 100644 --- a/application/forms/BpConfigForm.php +++ b/application/forms/BpConfigForm.php @@ -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(); diff --git a/application/forms/BpUploadForm.php b/application/forms/BpUploadForm.php index af4af43..ee3faf3 100644 --- a/application/forms/BpUploadForm.php +++ b/application/forms/BpUploadForm.php @@ -10,8 +10,6 @@ use Icinga\Web\Notification; class BpUploadForm extends BpConfigBaseForm { - protected $backend; - protected $node; protected $objectList = array(); diff --git a/application/forms/DeleteNodeForm.php b/application/forms/DeleteNodeForm.php index 67635bb..30fcdd4 100644 --- a/application/forms/DeleteNodeForm.php +++ b/application/forms/DeleteNodeForm.php @@ -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); diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index b22117e..aea064a 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -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; } diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php index 8e77f87..7396277 100644 --- a/application/forms/MoveNodeForm.php +++ b/application/forms/MoveNodeForm.php @@ -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(); diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php index cbc4466..69ab1a6 100644 --- a/application/forms/ProcessForm.php +++ b/application/forms/ProcessForm.php @@ -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); diff --git a/application/forms/SimulationForm.php b/application/forms/SimulationForm.php index 47c9f52..3d43e3a 100644 --- a/application/forms/SimulationForm.php +++ b/application/forms/SimulationForm.php @@ -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; diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php index 43e8f5b..977186a 100644 --- a/library/Businessprocess/BpConfig.php +++ b/library/Businessprocess/BpConfig.php @@ -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; } diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index c729c7c..ec248c2 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -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[] */ diff --git a/library/Businessprocess/Common/Sort.php b/library/Businessprocess/Common/Sort.php new file mode 100644 index 0000000..3b0f6d4 --- /dev/null +++ b/library/Businessprocess/Common/Sort.php @@ -0,0 +1,154 @@ +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; + } +} diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php index 9be77e9..4ad53e0 100644 --- a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php +++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php @@ -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()) + )); } } diff --git a/library/Businessprocess/Modification/NodeCopyAction.php b/library/Businessprocess/Modification/NodeCopyAction.php index 609d704..80d781b 100644 --- a/library/Businessprocess/Modification/NodeCopyAction.php +++ b/library/Businessprocess/Modification/NodeCopyAction.php @@ -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); } } diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php index 5754717..4c4305d 100644 --- a/library/Businessprocess/Modification/NodeMoveAction.php +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -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( diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 94a9667..6e68da4 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -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 */ diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php index df53989..21c2f6a 100644 --- a/library/Businessprocess/Renderer/TileRenderer.php +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -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)); } diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index c5733af..c9dd218 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -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 { diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php index 6582ebd..f6cf1e5 100644 --- a/library/Businessprocess/Storage/LegacyStorage.php +++ b/library/Businessprocess/Storage/LegacyStorage.php @@ -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; } diff --git a/library/Businessprocess/Web/Controller.php b/library/Businessprocess/Web/Controller.php index e9719e4..4f618b2 100644 --- a/library/Businessprocess/Web/Controller.php +++ b/library/Businessprocess/Web/Controller.php @@ -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(); diff --git a/library/Businessprocess/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php index ddfc851..5ccdf06 100644 --- a/library/Businessprocess/Web/Form/BpConfigBaseForm.php +++ b/library/Businessprocess/Web/Form/BpConfigBaseForm.php @@ -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; + } } diff --git a/public/css/module.less b/public/css/module.less index 95617e5..a05299a 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -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%; diff --git a/public/js/module.js b/public/js/module.js index 8bc6223..4855c9c 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -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(); } },