From 5245262a84db15d8576d134c1f6e8c8f02fd16a5 Mon Sep 17 00:00:00 2001 From: Bastian Lederer Date: Tue, 5 May 2026 14:19:21 +0200 Subject: [PATCH 1/5] Introduce `RuleSerializer` and `RuleParser` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RuleSerializer` serializes rules as json `RuleParser` can read them and create an ipl `Filter\Ŗule` from it --- library/Notifications/Util/RuleParser.php | 67 ++++++++++++ library/Notifications/Util/RuleSerializer.php | 100 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 library/Notifications/Util/RuleParser.php create mode 100644 library/Notifications/Util/RuleSerializer.php diff --git a/library/Notifications/Util/RuleParser.php b/library/Notifications/Util/RuleParser.php new file mode 100644 index 000000000..b9a6c33d4 --- /dev/null +++ b/library/Notifications/Util/RuleParser.php @@ -0,0 +1,67 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Notifications\Util; + +use ipl\Stdlib\Filter; + +class RuleParser +{ + public function parseJson(string $json): Filter\Rule + { + return $this->parseRule(json_decode($json, true)); + } + + /** + * Parse a serialized rule + * + * @param array $rule A rule serialized as json + * + * @return Filter\Rule + */ + public function parseRule(array $rule): Filter\Rule + { + if (in_array($rule['op'], ['&', '|', '!'])) { + return $this->parseChain($rule); + } else { + return $this->parseCondition($rule); + } + } + + protected function parseChain(array $data): Filter\Chain + { + $rules = []; + foreach ($data['rules'] as $rule) { + $rules[] = $this->parseRule($rule); + } + + + return match ($data['op']) { + '&' => new Filter\All(...$rules), + '|' => new Filter\Any(...$rules), + '!' => new Filter\None(...$rules), + }; + } + + protected function parseCondition(array $data): Filter\Condition + { + $condition = match ($data['op']) { + '!~' => new Filter\Unlike($data['column'], $data['value']), + '!=' => new Filter\Unequal($data['column'], $data['value']), + '~' => new Filter\Like($data['column'], $data['value']), + '=' => new Filter\Equal($data['column'], $data['value']), + '>' => new Filter\GreaterThan($data['column'], $data['value']), + '<' => new Filter\LessThan($data['column'], $data['value']), + '>=' => new Filter\GreaterThanOrEqual($data['column'], $data['value']), + '<=' => new Filter\LessThanOrEqual($data['column'], $data['value']), + }; + + if (isset($data['jsonPath'])) { + $condition->metaData()->set('jsonPath', $data['jsonPath']); + } + + return $condition; + } +} diff --git a/library/Notifications/Util/RuleSerializer.php b/library/Notifications/Util/RuleSerializer.php new file mode 100644 index 000000000..325e3d91f --- /dev/null +++ b/library/Notifications/Util/RuleSerializer.php @@ -0,0 +1,100 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Notifications\Util; + +use Icinga\Util\Json; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Filter\Chain; + +class RuleSerializer +{ + protected Filter\Rule $filter; + + /** + * Create an object that can be used to serialize a rule to JSON + * + * @param Filter\Rule $filter + */ + public function __construct(Filter\Rule $filter) + { + $this->filter = $filter; + } + + /** + * Serialize the filter as Json + * + * @return string + * + * @throws \Icinga\Exception\Json\JsonEncodeException + */ + public function getJson(): string + { + if ($this->filter instanceof Filter\Chain) { + $result = $this->serializeChain($this->filter); + } else { + /** @var Filter\Condition $this->filter */ + $result = $this->serializeCondition($this->filter); + } + + return Json::encode($result); + } + + /** + * Create an array with keys `op` and `rules` from a chain + * + * @param Chain $chain + * + * @return array{op: string, rules: array} + */ + protected function serializeChain(Chain $chain): array + { + $result = [ + 'op' => match (true) { + $chain instanceof Filter\All => '&', + $chain instanceof Filter\None => '!', + $chain instanceof Filter\Any => '|' + } + ]; + + $rules = []; + foreach ($chain as $rule) { + if ($rule instanceof Chain) { + $rules[] = $this->serializeChain($rule); + } else { + $rules[] = $this->serializeCondition($rule); + } + } + + $result['rules'] = $rules; + return $result; + } + + /** + * Create an array with the keys `op`, `column` and `value` from a condition + * + * @param Filter\Condition $condition + * + * @return array{op: string, column: string, value: mixed} + */ + protected function serializeCondition(Filter\Condition $condition): array + { + return [ + 'op' => match (true) { + $condition instanceof Filter\Unlike => '!~', + $condition instanceof Filter\Unequal => '!=', + $condition instanceof Filter\Like => '~', + $condition instanceof Filter\Equal => '=', + $condition instanceof Filter\GreaterThan => '>', + $condition instanceof Filter\LessThan => '<', + $condition instanceof Filter\GreaterThanOrEqual => '>=', + $condition instanceof Filter\LessThanOrEqual => '<=', + }, + 'column' => $condition->getColumn(), + 'value' => $condition->getValue(), + 'jsonPath' => $condition->metaData()->get('jsonPath'), + ]; + } +} From 4dca19173d4d49a99c5306c8806340ddff3fbb17 Mon Sep 17 00:00:00 2001 From: Bastian Lederer Date: Tue, 5 May 2026 14:21:52 +0200 Subject: [PATCH 2/5] Introduce `V2\SourceHook` Introduce a new version of the `SourceHook` interface for v 1.0 --- application/forms/SourceForm.php | 4 +- library/Notifications/Hook/V2/SourceHook.php | 68 ++++++++++++++++++++ library/Notifications/Model/Source.php | 4 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 library/Notifications/Hook/V2/SourceHook.php diff --git a/application/forms/SourceForm.php b/application/forms/SourceForm.php index 15fa3ab8c..aa2084e5f 100644 --- a/application/forms/SourceForm.php +++ b/application/forms/SourceForm.php @@ -9,7 +9,7 @@ use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Exception\Http\HttpNotFoundException; -use Icinga\Module\Notifications\Hook\V1\SourceHook; +use Icinga\Module\Notifications\Hook\V2\SourceHook; use Icinga\Module\Notifications\Model\Source; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; @@ -57,7 +57,7 @@ protected function assemble(): void self::TYPE_GENERIC => $this->translate('Generic') ]; - foreach (Hook::all('Notifications/v1/Source') as $hook) { + foreach (Hook::all('Notifications/v2/Source') as $hook) { /** @var SourceHook $hook */ try { $type = $hook->getSourceType(); diff --git a/library/Notifications/Hook/V2/SourceHook.php b/library/Notifications/Hook/V2/SourceHook.php new file mode 100644 index 000000000..aa1d29228 --- /dev/null +++ b/library/Notifications/Hook/V2/SourceHook.php @@ -0,0 +1,68 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Notifications\Hook\V2; + +use ipl\Stdlib\Filter\Condition; +use ipl\Web\Control\SearchBar\Suggestions; +use ipl\Web\Widget\Icon; + +interface SourceHook +{ + /** + * Get the type of source this integration is responsible for + * + * @return string + */ + public function getSourceType(): string; + + /** + * Get the label of the source this integration is responsible for + * + * @return string + */ + public function getSourceLabel(): string; + + /** + * Get the icon of the source this integration is responsible for + * + * @return Icon + */ + public function getSourceIcon(): Icon; + + /** + * Get whether the condition is valid + * + * @param Condition $condition + * + * @return bool + */ + public function isValidCondition(Condition $condition): bool; + + /** + * Enrich the given condition with metadata like the columnLabel + * + * @param Condition $condition + * + * @return void + */ + public function enrichCondition(Condition $condition): void; + + /** + * Get the JsonPath metadata for a condition + * + * @param Condition $condition + * + * @return string + */ + public function getJsonPath(Condition $condition): string; + + /** + * Get the Suggestions for the editor + * + * @return Suggestions + */ + public function getSuggestions(): Suggestions; +} diff --git a/library/Notifications/Model/Source.php b/library/Notifications/Model/Source.php index d69bbee2e..557d33adf 100644 --- a/library/Notifications/Model/Source.php +++ b/library/Notifications/Model/Source.php @@ -8,7 +8,7 @@ use DateTime; use Icinga\Application\Hook; use Icinga\Application\Logger; -use Icinga\Module\Notifications\Hook\V1\SourceHook; +use Icinga\Module\Notifications\Hook\V2\SourceHook; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; @@ -103,7 +103,7 @@ public function getIcon(): Icon // Fallback, in case an integration is inactive or missing $icon = new Icon('share-nodes'); - foreach (Hook::all('Notifications/v1/Source') as $hook) { + foreach (Hook::all('Notifications/v2/Source') as $hook) { /** @var SourceHook $hook */ try { if ($hook->getSourceType() === $this->type) { From ae3ff2f96095b92a2607d537e33d4d99de6e1b95 Mon Sep 17 00:00:00 2001 From: Bastian Lederer Date: Tue, 5 May 2026 14:23:38 +0200 Subject: [PATCH 3/5] `EventRuleController`: Adjust to new `SourceHook` Use the functions of the new `SourceHook` interface, the `RuleParser` and `RuleSerializer` to set up the filter editor of an event rule- --- .../controllers/EventRuleController.php | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index 69b255925..c736653a2 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -7,6 +7,7 @@ use Icinga\Application\Hook; use Icinga\Application\Logger; +use Icinga\Exception\ConfigurationError; use Icinga\Exception\Http\HttpNotFoundException; use Icinga\Module\Notifications\Common\Auth; use Icinga\Module\Notifications\Common\Database; @@ -14,20 +15,25 @@ use Icinga\Module\Notifications\Forms\EventRuleConfigElements\NotificationConfigProvider; use Icinga\Module\Notifications\Forms\EventRuleConfigForm; use Icinga\Module\Notifications\Forms\EventRuleForm; -use Icinga\Module\Notifications\Hook\V1\SourceHook; +use Icinga\Module\Notifications\Hook\V2\SourceHook; use Icinga\Module\Notifications\Model\Rule; use Icinga\Module\Notifications\Model\Source; +use Icinga\Module\Notifications\Util\RuleParser; +use Icinga\Module\Notifications\Util\RuleSerializer; use Icinga\Module\Notifications\Web\Control\SearchBar\ExtraTagSuggestions; use Icinga\Web\Notification; use Icinga\Web\Session; use ipl\Html\Contract\Form; use ipl\Html\Html; use ipl\Stdlib\Filter; +use ipl\Stdlib\Filter\Condition; use ipl\Web\Compat\CompatController; -use ipl\Web\Compat\CompatForm; +use ipl\Web\Control\SearchBar\SearchException; +use ipl\Web\Control\SearchEditor; use ipl\Web\Url; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; +use JsonException; use Psr\Http\Message\ServerRequestInterface; use Throwable; @@ -195,7 +201,71 @@ public function searchEditorAction(): void { $ruleId = (int) $this->params->getRequired('id'); $filter = $this->params->get('object_filter', $this->session->get('object_filter')); + $hook = $this->resolveSourceHook($ruleId); + if ($filter) { + try { + $parsedFilter = (new RuleParser())->parseJson($filter); + $applyLabels = function (Filter\Rule $rule) use ($hook, &$applyLabels): void { + if ($rule instanceof Filter\Chain) { + foreach ($rule as $child) { + $applyLabels($child); + } + } else { + /** @var Condition $rule */ + $hook->enrichCondition($rule); + } + }; + + $applyLabels($parsedFilter); + } catch (JsonException $e) { + Logger::error('Failed to parse rule filter configuration: %s (Error: %s)', $filter, $e); + throw new ConfigurationError($this->translate( + 'Failed to parse rule filter configuration. Please contact your system administrator.' + )); + } + } + + $editor = (new SearchEditor()) + ->setFilter($parsedFilter ?? new Filter\All()) + ->setSuggestionUrl( + Url::fromPath( + 'notifications/event-rule/suggest', + ['id' => $ruleId, '_disableLayout' => true, 'showCompact' => true] + ) + ) + ->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl()) + ->setMetadataFields(['customvarid']) + ->on( + SearchEditor::ON_VALIDATE_COLUMN, + function (Condition $condition) use ($hook) { + if (! $hook->isValidCondition($condition)) { + throw new SearchException($this->translate('Is not a valid column')); + } + + $condition->metaData()->set('jsonPath', $hook->getJsonPath($condition)); + } + ) + ->on(Form::ON_SUBMIT, function (SearchEditor $form) use ($ruleId, $hook) { + $this->session->set('object_filter', (new RuleSerializer($form->getFilter()))->getJson()); + $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); + }) + ->handleRequest($this->getServerRequest()); + + $this->getDocument()->addHtml($editor); + + $this->setTitle($this->translate('Adjust Filter')); + } + + public function suggestAction(): void + { + $hook = $this->resolveSourceHook((int) $this->params->getRequired('id')); + $suggestions = $hook->getSuggestions(); + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } + + protected function resolveSourceHook(int $ruleId): SourceHook + { $source = null; if ($ruleId !== -1) { $source = Rule::on(Database::get()) @@ -217,7 +287,7 @@ public function searchEditorAction(): void } $hook = null; - foreach (Hook::all('Notifications/v1/Source') as $h) { + foreach (Hook::all('Notifications/v2/Source') as $h) { /** @var SourceHook $h */ try { if ($h->getSourceType() === $source->type) { @@ -237,51 +307,7 @@ public function searchEditorAction(): void ), $source->type)); } - if (! $filter) { - $targets = $hook->getRuleFilterTargets($source->id); - if (count($targets) === 1 && ! is_array(reset($targets))) { - $filter = key($targets); - } else { - $target = null; - $form = (new CompatForm()) - ->applyDefaultElementDecorators() - ->setAction(Url::fromRequest()->getAbsoluteUrl()) - ->addElement('select', 'target', [ - 'required' => true, - 'label' => $this->translate('Filter Target'), - 'options' => ['' => ' - ' . $this->translate('Please choose') . ' - '] + $targets, - 'disabledOptions' => [''] - ]) - ->addElement('submit', 'btn_submit', [ - // translators: shown on a submit button to proceed to the next step of a form wizard - 'label' => $this->translate('Next') - ]) - ->on(Form::ON_SUBMIT, function (CompatForm $form) use (&$target) { - $target = $form->getValue('target'); - }) - ->handleRequest($this->getServerRequest()); - - if ($target !== null) { - $filter = $target; - } else { - $this->addContent($form); - } - } - } - - if ($filter) { - $form = $hook->getRuleFilterEditor($filter) - ->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl()) - ->on(Form::ON_SUBMIT, function (Form $form) use ($ruleId, $hook) { - $this->session->set('object_filter', $hook->serializeRuleFilter($form)); - $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); - }) - ->handleRequest($this->getServerRequest()); - - $this->getDocument()->addHtml($form); - } - - $this->setTitle($this->translate('Adjust Filter')); + return $hook; } public function editAction(): void From 3bf7acbf28e62cb022f84414a54af4177980da24 Mon Sep 17 00:00:00 2001 From: Bastian Lederer Date: Tue, 5 May 2026 15:50:50 +0200 Subject: [PATCH 4/5] Ensure any metadata keys are supported --- .../controllers/EventRuleController.php | 7 +++-- library/Notifications/Hook/V2/SourceHook.php | 9 +++++++ library/Notifications/Util/RuleParser.php | 22 ++++++++------- library/Notifications/Util/RuleSerializer.php | 27 ++++++++++++++++--- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index c736653a2..7f67f7566 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -235,7 +235,7 @@ public function searchEditorAction(): void ) ) ->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl()) - ->setMetadataFields(['customvarid']) + ->setMetadataFields($hook->getMetadataKeys()) ->on( SearchEditor::ON_VALIDATE_COLUMN, function (Condition $condition) use ($hook) { @@ -247,7 +247,10 @@ function (Condition $condition) use ($hook) { } ) ->on(Form::ON_SUBMIT, function (SearchEditor $form) use ($ruleId, $hook) { - $this->session->set('object_filter', (new RuleSerializer($form->getFilter()))->getJson()); + $this->session->set( + 'object_filter', + (new RuleSerializer($form->getFilter(), $hook->getMetadataKeys()))->getJson() + ); $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); }) ->handleRequest($this->getServerRequest()); diff --git a/library/Notifications/Hook/V2/SourceHook.php b/library/Notifications/Hook/V2/SourceHook.php index aa1d29228..0b8a9c1c1 100644 --- a/library/Notifications/Hook/V2/SourceHook.php +++ b/library/Notifications/Hook/V2/SourceHook.php @@ -65,4 +65,13 @@ public function getJsonPath(Condition $condition): string; * @return Suggestions */ public function getSuggestions(): Suggestions; + + /** + * Get the metadata keys for the conditions + * + * Only the given keys will be added to the SearchEditor and stored in the database + * + * @return string[] + */ + public function getMetadataKeys(): array; } diff --git a/library/Notifications/Util/RuleParser.php b/library/Notifications/Util/RuleParser.php index b9a6c33d4..54fd6d83f 100644 --- a/library/Notifications/Util/RuleParser.php +++ b/library/Notifications/Util/RuleParser.php @@ -48,18 +48,20 @@ protected function parseChain(array $data): Filter\Chain protected function parseCondition(array $data): Filter\Condition { $condition = match ($data['op']) { - '!~' => new Filter\Unlike($data['column'], $data['value']), - '!=' => new Filter\Unequal($data['column'], $data['value']), - '~' => new Filter\Like($data['column'], $data['value']), - '=' => new Filter\Equal($data['column'], $data['value']), - '>' => new Filter\GreaterThan($data['column'], $data['value']), - '<' => new Filter\LessThan($data['column'], $data['value']), - '>=' => new Filter\GreaterThanOrEqual($data['column'], $data['value']), - '<=' => new Filter\LessThanOrEqual($data['column'], $data['value']), + '!~' => new Filter\Unlike($data['columnName'], $data['value']), + '!=' => new Filter\Unequal($data['columnName'], $data['value']), + '~' => new Filter\Like($data['columnName'], $data['value']), + '=' => new Filter\Equal($data['columnName'], $data['value']), + '>' => new Filter\GreaterThan($data['columnName'], $data['value']), + '<' => new Filter\LessThan($data['columnName'], $data['value']), + '>=' => new Filter\GreaterThanOrEqual($data['columnName'], $data['value']), + '<=' => new Filter\LessThanOrEqual($data['columnName'], $data['value']), }; - if (isset($data['jsonPath'])) { - $condition->metaData()->set('jsonPath', $data['jsonPath']); + $condition->metaData()->set('jsonPath', $data['column']); + + foreach ($data['metadata'] ?? [] as $key => $value) { + $condition->metaData()->set($key, $value); } return $condition; diff --git a/library/Notifications/Util/RuleSerializer.php b/library/Notifications/Util/RuleSerializer.php index 325e3d91f..4db2a4981 100644 --- a/library/Notifications/Util/RuleSerializer.php +++ b/library/Notifications/Util/RuleSerializer.php @@ -13,14 +13,17 @@ class RuleSerializer { protected Filter\Rule $filter; + protected array $metadataKeys = []; + /** * Create an object that can be used to serialize a rule to JSON * * @param Filter\Rule $filter */ - public function __construct(Filter\Rule $filter) + public function __construct(Filter\Rule $filter, array $metadataKeys = []) { $this->filter = $filter; + $this->metadataKeys = $metadataKeys; } /** @@ -92,9 +95,27 @@ protected function serializeCondition(Filter\Condition $condition): array $condition instanceof Filter\GreaterThanOrEqual => '>=', $condition instanceof Filter\LessThanOrEqual => '<=', }, - 'column' => $condition->getColumn(), + 'column' => $condition->metaData()->get('jsonPath'), 'value' => $condition->getValue(), - 'jsonPath' => $condition->metaData()->get('jsonPath'), + 'columnName' => $condition->getColumn(), + 'metadata' => $this->serializeConditionMetadata($condition), ]; } + + /** + * Serialize the metadata of a condtion into an array + * + * @param Filter\Condition $condition + * + * @return array + */ + protected function serializeConditionMetadata(Filter\Condition $condition): array + { + $result = []; + foreach ($this->metadataKeys as $key) { + $result[$key] = $condition->metaData()->get($key); + } + + return $result; + } } From 889baa635aeafd19d1feb3c73be3c928bd2c2570 Mon Sep 17 00:00:00 2001 From: Bastian Lederer Date: Thu, 7 May 2026 13:40:16 +0200 Subject: [PATCH 5/5] `EventRuleController`: Support configuration of generic sources Adjust `resolveSourceHook` to return `null` for generic sources without integration. Adjust `searchEditorAction` to create a simple editor without validation, enrichment and suggestions for these sources and save the filter as a querystring. --- .../controllers/EventRuleController.php | 117 +++++++++++------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index 7f67f7566..e46ed7b1e 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -30,6 +30,7 @@ use ipl\Web\Compat\CompatController; use ipl\Web\Control\SearchBar\SearchException; use ipl\Web\Control\SearchEditor; +use ipl\Web\Filter\Renderer; use ipl\Web\Url; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; @@ -202,59 +203,77 @@ public function searchEditorAction(): void $ruleId = (int) $this->params->getRequired('id'); $filter = $this->params->get('object_filter', $this->session->get('object_filter')); $hook = $this->resolveSourceHook($ruleId); - - if ($filter) { - try { - $parsedFilter = (new RuleParser())->parseJson($filter); - $applyLabels = function (Filter\Rule $rule) use ($hook, &$applyLabels): void { - if ($rule instanceof Filter\Chain) { - foreach ($rule as $child) { - $applyLabels($child); + $editor = (new SearchEditor()) + ->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl()); + + if ($hook) { + if ($filter) { + try { + $parsedFilter = (new RuleParser())->parseJson($filter); + $applyLabels = function (Filter\Rule $rule) use ($hook, &$applyLabels): void { + if ($rule instanceof Filter\Chain) { + foreach ($rule as $child) { + $applyLabels($child); + } + } else { + /** @var Condition $rule */ + $hook->enrichCondition($rule); } - } else { - /** @var Condition $rule */ - $hook->enrichCondition($rule); - } - }; - - $applyLabels($parsedFilter); - } catch (JsonException $e) { - Logger::error('Failed to parse rule filter configuration: %s (Error: %s)', $filter, $e); - throw new ConfigurationError($this->translate( - 'Failed to parse rule filter configuration. Please contact your system administrator.' - )); + }; + + $applyLabels($parsedFilter); + } catch (JsonException $e) { + Logger::error('Failed to parse rule filter configuration: %s (Error: %s)', $filter, $e); + throw new ConfigurationError( + $this->translate( + 'Failed to parse rule filter configuration. Please contact your system administrator.' + ) + ); + } } - } - $editor = (new SearchEditor()) - ->setFilter($parsedFilter ?? new Filter\All()) - ->setSuggestionUrl( - Url::fromPath( - 'notifications/event-rule/suggest', - ['id' => $ruleId, '_disableLayout' => true, 'showCompact' => true] + $editor + ->setFilter($parsedFilter ?? new Filter\All()) + ->setSuggestionUrl( + Url::fromPath( + 'notifications/event-rule/suggest', + ['id' => $ruleId, '_disableLayout' => true, 'showCompact' => true] + ) ) - ) - ->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl()) - ->setMetadataFields($hook->getMetadataKeys()) - ->on( - SearchEditor::ON_VALIDATE_COLUMN, - function (Condition $condition) use ($hook) { - if (! $hook->isValidCondition($condition)) { - throw new SearchException($this->translate('Is not a valid column')); - } + ->setMetadataFields($hook->getMetadataKeys()) + ->on( + SearchEditor::ON_VALIDATE_COLUMN, + function (Condition $condition) use ($hook) { + if (! $hook->isValidCondition($condition)) { + throw new SearchException($this->translate('Is not a valid column')); + } - $condition->metaData()->set('jsonPath', $hook->getJsonPath($condition)); - } - ) - ->on(Form::ON_SUBMIT, function (SearchEditor $form) use ($ruleId, $hook) { - $this->session->set( - 'object_filter', - (new RuleSerializer($form->getFilter(), $hook->getMetadataKeys()))->getJson() + $condition->metaData()->set('jsonPath', $hook->getJsonPath($condition)); + } + ) + ->on(Form::ON_SUBMIT, function (SearchEditor $form) use ($ruleId, $hook) { + $this->session->set( + 'object_filter', + (new RuleSerializer($form->getFilter(), $hook->getMetadataKeys()))->getJson() + ); + $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); + }); + } else { + $editor + ->setQueryString($filter ?? '') + ->on( + Form::ON_SUBMIT, + function (SearchEditor $editor) use ($ruleId) { + $this->session->set( + 'object_filter', + (new Renderer($editor->getFilter()))->render() + ); + $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); + } ); - $this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly')); - }) - ->handleRequest($this->getServerRequest()); + } + $editor->handleRequest($this->getServerRequest()); $this->getDocument()->addHtml($editor); $this->setTitle($this->translate('Adjust Filter')); @@ -267,7 +286,7 @@ public function suggestAction(): void $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); } - protected function resolveSourceHook(int $ruleId): SourceHook + protected function resolveSourceHook(int $ruleId): ?SourceHook { $source = null; if ($ruleId !== -1) { @@ -289,6 +308,10 @@ protected function resolveSourceHook(int $ruleId): SourceHook $this->httpNotFound($this->translate('Rule not found')); } + if ($source->type === 'generic') { + return null; + } + $hook = null; foreach (Hook::all('Notifications/v2/Source') as $h) { /** @var SourceHook $h */