From 1ac87cb4ea3c414bda77e2c5ff165d559fc727a5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 25 Jul 2023 12:58:16 +0200 Subject: [PATCH 1/7] Let all config forms extend `BpConfigBaseForm` They're all too similar. --- application/controllers/ProcessController.php | 2 +- application/forms/AddNodeForm.php | 62 +---------------- application/forms/BpConfigForm.php | 12 ++-- application/forms/BpUploadForm.php | 2 - application/forms/DeleteNodeForm.php | 48 +------------- application/forms/EditNodeForm.php | 48 +------------- application/forms/ProcessForm.php | 48 +------------- application/forms/SimulationForm.php | 4 +- .../Web/Form/BpConfigBaseForm.php | 66 +++++++++++++++++-- 9 files changed, 77 insertions(+), 215 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index ef525c6..6f74be5 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -510,7 +510,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() diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 448250c..2f72c6b 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -4,31 +4,17 @@ 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\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; - /** @var BpNode */ protected $parent; @@ -36,9 +22,6 @@ class AddNodeForm extends QuickForm protected $processList = array(); - /** @var SessionNamespace */ - protected $session; - public function setup() { $view = $this->getView(); @@ -389,37 +372,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 +390,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; 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..6b73044 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; 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/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php index ddfc851..5d7e2cd 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; } From 926ac14bd9e123cd01c0f11878251be871aec220 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 25 Jul 2023 13:05:38 +0200 Subject: [PATCH 2/7] css: Apply form specific rules only to `form.bp-form` --- .../Web/Form/BpConfigBaseForm.php | 9 + public/css/module.less | 321 +++++++++--------- 2 files changed, 170 insertions(+), 160 deletions(-) diff --git a/library/Businessprocess/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php index 5d7e2cd..5ccdf06 100644 --- a/library/Businessprocess/Web/Form/BpConfigBaseForm.php +++ b/library/Businessprocess/Web/Form/BpConfigBaseForm.php @@ -123,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..deb9e3a 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -790,177 +790,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%; From 8b2bac4e85865978e52dc1dbb497cd3c762fafb5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 26 Jul 2023 16:55:09 +0200 Subject: [PATCH 3/7] MoveNodeForm: Instruct JS to refresh the target container instead of letting JS refresh it on its own --- application/controllers/ProcessController.php | 7 +++++++ application/forms/MoveNodeForm.php | 6 +++++- public/js/module.js | 12 ++---------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 6f74be5..915aa6b 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -263,7 +263,14 @@ 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'); + } + $form = $this->loadForm('MoveNode') + ->setSuccessUrl($successUrl) ->setProcess($bp) ->setParentNode($node) ->setSession($this->session()) 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/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(); } }, From 96f94f995939ef4b3f20fe919ff0ed0e53446e43 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 26 Jul 2023 16:56:39 +0200 Subject: [PATCH 4/7] Controller: Extend `ipl\Web\Compat\CompatController` --- library/Businessprocess/Web/Controller.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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(); From c3d09fd6015a4041d191a62f4ce5f5011784a5e7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 26 Jul 2023 17:04:50 +0200 Subject: [PATCH 5/7] Introduce `Icinga\Module\Businessprocess\Common\Sort` --- library/Businessprocess/Common/Sort.php | 154 ++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 library/Businessprocess/Common/Sort.php 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; + } +} From 52c150c56b0dcbc5339cc1da3d92024d3278f21b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 26 Jul 2023 17:15:26 +0200 Subject: [PATCH 6/7] Use the new `Sort` trait where applicable Moves the entire order processing to the renderers as that's where it's mostly relevant. The only cases where nodes are still ordered outside the rendering is where changes are applied based on user input, which happened based on what's been previously rendered. --- application/forms/AddNodeForm.php | 9 +++-- application/forms/EditNodeForm.php | 3 -- library/Businessprocess/BpConfig.php | 33 +------------------ library/Businessprocess/BpNode.php | 26 --------------- .../NodeApplyManualOrderAction.php | 8 ++++- .../Modification/NodeCopyAction.php | 13 ++++++-- .../Modification/NodeMoveAction.php | 25 +++++++++++--- library/Businessprocess/Renderer/Renderer.php | 31 +++++++++++++++++ .../Businessprocess/Renderer/TileRenderer.php | 8 ++--- .../Businessprocess/Renderer/TreeRenderer.php | 18 ++++++---- .../Businessprocess/Storage/LegacyStorage.php | 2 -- 11 files changed, 89 insertions(+), 87 deletions(-) diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 2f72c6b..bab26bb 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Forms; use Exception; use Icinga\Module\Businessprocess\BpNode; 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; @@ -14,6 +15,7 @@ use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidato class AddNodeForm extends BpConfigBaseForm { use EnumList; + use Sort; /** @var BpNode */ protected $parent; @@ -102,8 +104,8 @@ class AddNodeForm extends BpConfigBaseForm )); $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( @@ -434,9 +436,6 @@ class AddNodeForm extends BpConfigBaseForm } } - if (! $this->bp->getMetadata()->isManuallyOrdered()) { - natcasesort($list); - } return $list; } diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index 6b73044..aea064a 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -310,9 +310,6 @@ class EditNodeForm extends BpConfigBaseForm } } - if (! $this->bp->getMetadata()->isManuallyOrdered()) { - natcasesort($list); - } return $list; } 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/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; } From 2acf6118123a98289480696e707df48dad45b2a9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 26 Jul 2023 17:23:40 +0200 Subject: [PATCH 7/7] process/show: Allow to adjust the order of nodes Nodes can be ordered as usually by display name, but now also in descending order. They can now also be ordered by state. This also applies to manually ordered processes. Though, changes to the manual order can only happen if the default order is active. If that's not the case, a note is shown and a way to reset the sort order. --- application/controllers/ProcessController.php | 76 ++++++++++++++++++- public/css/module.less | 8 +- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 915aa6b..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) @@ -269,6 +318,13 @@ class ProcessController extends Controller $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) @@ -328,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); @@ -369,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 { @@ -654,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/public/css/module.less b/public/css/module.less index deb9e3a..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;