Skip to content

Commit 626d0c6

Browse files
authored
Source bound event rules (#373)
resolves #354
2 parents c5aeb3b + 79e9271 commit 626d0c6

File tree

18 files changed

+690
-448
lines changed

18 files changed

+690
-448
lines changed

application/controllers/EventRuleController.php

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,31 @@
44

55
namespace Icinga\Module\Notifications\Controllers;
66

7+
use Icinga\Application\Hook;
8+
use Icinga\Application\Logger;
79
use Icinga\Exception\Http\HttpNotFoundException;
810
use Icinga\Module\Notifications\Common\Auth;
911
use Icinga\Module\Notifications\Common\Database;
1012
use Icinga\Module\Notifications\Common\Links;
1113
use Icinga\Module\Notifications\Forms\EventRuleConfigElements\NotificationConfigProvider;
1214
use Icinga\Module\Notifications\Forms\EventRuleConfigForm;
1315
use Icinga\Module\Notifications\Forms\EventRuleForm;
16+
use Icinga\Module\Notifications\Hook\V1\SourceHook;
1417
use Icinga\Module\Notifications\Model\Rule;
18+
use Icinga\Module\Notifications\Model\Source;
1519
use Icinga\Module\Notifications\Web\Control\SearchBar\ExtraTagSuggestions;
1620
use Icinga\Web\Notification;
1721
use Icinga\Web\Session;
18-
use ipl\Html\Form;
22+
use ipl\Html\Contract\Form;
1923
use ipl\Html\Html;
2024
use ipl\Stdlib\Filter;
2125
use ipl\Web\Compat\CompatController;
22-
use ipl\Web\Control\SearchEditor;
23-
use ipl\Web\Filter\Renderer;
26+
use ipl\Web\Compat\CompatForm;
2427
use ipl\Web\Url;
2528
use ipl\Web\Widget\Icon;
2629
use ipl\Web\Widget\Link;
2730
use Psr\Http\Message\ServerRequestInterface;
31+
use Throwable;
2832

2933
class EventRuleController extends CompatController
3034
{
@@ -55,7 +59,7 @@ public function indexAction(): void
5559
))->setCsrfCounterMeasureId(Session::getSession()->getId());
5660

5761
$eventRuleConfig
58-
->on(Form::ON_SUCCESS, function (EventRuleConfigForm $form) use ($ruleId) {
62+
->on(Form::ON_SUBMIT, function (EventRuleConfigForm $form) use ($ruleId) {
5963
if ($ruleId !== -1) {
6064
$rule = $this->fetchRule($ruleId);
6165
} else {
@@ -72,7 +76,9 @@ public function indexAction(): void
7276
})
7377
->on(Form::ON_SENT, function (EventRuleConfigForm $form) use ($ruleId) {
7478
if ($form->hasBeenRemoved()) {
75-
$form->removeRule(Database::get(), $this->fetchRule($ruleId));
79+
Database::get()->transaction(
80+
fn() => $form::removeRule(Database::get(), $this->fetchRule($ruleId))
81+
);
7682
Notification::success(sprintf(
7783
$this->translate('Successfully deleted event rule %s'),
7884
$form->getValue('name')
@@ -95,7 +101,10 @@ public function indexAction(): void
95101

96102
if ($nameOnly) {
97103
$this->addPart($form->prepareObjectFilterUpdate($this->session->get('object_filter')));
98-
$this->addPart($form->prepareNameUpdate($this->session->get('name')));
104+
$this->addPart($form->prepareConfigUpdate(
105+
$this->session->get('name'),
106+
$this->session->get('source')
107+
));
99108
$this->addPart(Html::tag('div', ['id' => 'event-rule-config-name'], [
100109
Html::tag('h2', $this->session->get('name')),
101110
(new Link(
@@ -105,7 +114,10 @@ public function indexAction(): void
105114
))->openInModal()
106115
]));
107116
} else {
108-
$this->addPart($form->prepareNameUpdate($this->session->get('name')));
117+
$this->addPart($form->prepareConfigUpdate(
118+
$this->session->get('name'),
119+
$this->session->get('source')
120+
));
109121
$this->addPart($form->prepareObjectFilterUpdate($this->session->get('object_filter')));
110122
}
111123

@@ -116,12 +128,15 @@ public function indexAction(): void
116128
$form->load($rule);
117129

118130
$this->session->set('name', $rule->name);
131+
$this->session->set('source', $rule->source_id);
119132
$this->session->set('object_filter', $rule->object_filter ?? '');
120133
} else {
121134
$name = $this->params->getRequired('name');
122-
$form->populate(['id' => $ruleId, 'name' => $name]);
135+
$source = $this->params->getRequired('source');
136+
$form->populate(['id' => $ruleId, 'name' => $name, 'source' => $source]);
123137

124138
$this->session->set('name', $name);
139+
$this->session->set('source', $source);
125140
$this->session->set('object_filter', '');
126141
}
127142
})
@@ -172,31 +187,93 @@ public function completeAction(): void
172187
public function searchEditorAction(): void
173188
{
174189
$ruleId = (int) $this->params->getRequired('id');
190+
$filter = $this->params->get('object_filter', $this->session->get('object_filter'));
191+
192+
$source = null;
193+
if ($ruleId !== -1) {
194+
$source = Rule::on(Database::get())
195+
->columns(['id' => 'source.id', 'type' => 'source.type'])
196+
->filter(Filter::all(
197+
Filter::equal('id', $ruleId),
198+
Filter::equal('deleted', 'n')
199+
))
200+
->first();
201+
} elseif (isset($this->session->source)) {
202+
$source = Source::on(Database::get())
203+
->columns(['id', 'type'])
204+
->filter(Filter::equal('id', $this->session->source))
205+
->first();
206+
}
175207

176-
$editor = new SearchEditor();
208+
if ($source === null) {
209+
$this->httpNotFound($this->translate('Rule not found'));
210+
}
211+
212+
$hook = null;
213+
foreach (Hook::all('Notifications/v1/Source') as $h) {
214+
/** @var SourceHook $h */
215+
try {
216+
if ($h->getSourceType() === $source->type) {
217+
$hook = $h;
218+
219+
break;
220+
}
221+
} catch (Throwable $e) {
222+
Logger::error('Failed to load source integration %s: %s', $h::class, $e);
223+
}
224+
}
225+
226+
if ($hook === null) {
227+
$this->httpNotFound(sprintf($this->translate(
228+
'No source integration available. Either the module supporting sources of type "%s" is not'
229+
. ' enabled or you have insufficient privileges. Please contact your system administrator.'
230+
), $source->type));
231+
}
232+
233+
if (! $filter) {
234+
$targets = $hook->getRuleFilterTargets($source->id);
235+
if (count($targets) === 1 && ! is_array(reset($targets))) {
236+
$filter = key($targets);
237+
} else {
238+
$target = null;
239+
$form = (new CompatForm())
240+
->applyDefaultElementDecorators()
241+
->setAction(Url::fromRequest()->getAbsoluteUrl())
242+
->addElement('select', 'target', [
243+
'required' => true,
244+
'label' => $this->translate('Filter Target'),
245+
'options' => ['' => ' - ' . $this->translate('Please choose') . ' - '] + $targets,
246+
'disabledOptions' => ['']
247+
])
248+
->addElement('submit', 'btn_submit', [
249+
// translators: shown on a submit button to proceed to the next step of a form wizard
250+
'label' => $this->translate('Next')
251+
])
252+
->on(Form::ON_SUBMIT, function (CompatForm $form) use (&$target) {
253+
$target = $form->getValue('target');
254+
})
255+
->handleRequest($this->getServerRequest());
256+
257+
if ($target !== null) {
258+
$filter = $target;
259+
} else {
260+
$this->addContent($form);
261+
}
262+
}
263+
}
264+
265+
if ($filter) {
266+
$form = $hook->getRuleFilterEditor($filter)
267+
->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl())
268+
->on(Form::ON_SUBMIT, function (Form $form) use ($ruleId, $hook) {
269+
$this->session->set('object_filter', $hook->serializeRuleFilter($form));
270+
$this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly'));
271+
})
272+
->handleRequest($this->getServerRequest());
273+
274+
$this->getDocument()->addHtml($form);
275+
}
177276

178-
$editor->setQueryString($this->session->get('object_filter'))
179-
->setAction(Url::fromRequest()->getAbsoluteUrl())
180-
->setSuggestionUrl(
181-
Url::fromPath('notifications/event-rule/complete', [
182-
'id' => $ruleId,
183-
'_disableLayout' => true,
184-
'showCompact' => true
185-
])
186-
);
187-
188-
$editor->on(Form::ON_SUCCESS, function (SearchEditor $form) use ($ruleId) {
189-
$filter = (new Renderer($form->getFilter()))->render();
190-
// TODO: Should not be needed for the new filter implementation
191-
$filter = preg_replace('/(?:=|~|!|%3[EC])(?=[|&]|$)/', '', $filter);
192-
193-
$this->session->set('object_filter', $filter);
194-
$this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly'));
195-
});
196-
197-
$editor->handleRequest($this->getServerRequest());
198-
199-
$this->getDocument()->addHtml($editor);
200277
$this->setTitle($this->translate('Adjust Filter'));
201278
}
202279

@@ -206,10 +283,25 @@ public function editAction(): void
206283

207284
$eventRuleForm = (new EventRuleForm())
208285
->setCsrfCounterMeasureId(Session::getSession()->getId())
209-
->populate(['name' => $this->session->get('name')])
286+
->setAvailableSources(
287+
Database::get()->fetchPairs(
288+
Source::on(Database::get())->columns(['id', 'name'])->assembleSelect()
289+
)
290+
)
291+
->populate([
292+
'name' => $this->session->get('name'),
293+
'source' => $this->session->get('source')
294+
])
210295
->setAction(Url::fromRequest()->getAbsoluteUrl())
211-
->on(Form::ON_SUCCESS, function ($form) use ($ruleId) {
296+
->on(Form::ON_SUBMIT, function ($form) use ($ruleId) {
212297
$this->session->set('name', $form->getValue('name'));
298+
299+
$newSource = $form->getValue('source');
300+
if ($newSource !== $this->session->get('source')) {
301+
$this->session->set('source', $newSource);
302+
$this->session->set('object_filter', '');
303+
}
304+
213305
$this->redirectNow(Links::eventRule($ruleId)->setParam('_nameOnly'));
214306
})->handleRequest($this->getServerRequest());
215307

application/controllers/EventRulesController.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@
88
use Icinga\Module\Notifications\Common\Links;
99
use Icinga\Module\Notifications\Forms\EventRuleForm;
1010
use Icinga\Module\Notifications\Model\Rule;
11+
use Icinga\Module\Notifications\Model\Source;
1112
use Icinga\Module\Notifications\View\EventRuleRenderer;
1213
use Icinga\Module\Notifications\Web\Control\SearchBar\ObjectSuggestions;
1314
use Icinga\Module\Notifications\Widget\ItemList\ObjectList;
1415
use Icinga\Web\Session;
1516
use ipl\Html\Form;
17+
use ipl\Html\TemplateString;
18+
use ipl\Sql\Expression;
1619
use ipl\Web\Compat\CompatController;
1720
use ipl\Web\Compat\SearchControls;
1821
use ipl\Web\Control\LimitControl;
1922
use ipl\Web\Control\SortControl;
2023
use ipl\Web\Filter\QueryString;
2124
use ipl\Web\Layout\DetailedItemLayout;
2225
use ipl\Web\Url;
26+
use ipl\Web\Widget\ActionLink;
2327
use ipl\Web\Widget\ButtonLink;
2428

2529
class EventRulesController extends CompatController
@@ -72,18 +76,34 @@ public function indexAction(): void
7276
$this->addControl($limitControl);
7377
$this->addControl($searchBar);
7478

75-
$this->addContent(
76-
(new ButtonLink(
77-
$this->translate('Add Event Rule'),
78-
Url::fromPath('notifications/event-rules/add'),
79-
'plus',
80-
['class' => 'add-new-component']
81-
))->openInModal()
79+
$addButton = new ButtonLink(
80+
$this->translate('Add Event Rule'),
81+
Url::fromPath('notifications/event-rules/add'),
82+
'plus',
83+
['class' => 'add-new-component']
8284
);
85+
$this->addContent($addButton);
86+
87+
$emptyStateMessage = null;
88+
if (Source::on(Database::get())->columns([new Expression('1')])->limit(1)->first() === null) {
89+
$addButton->disable($this->translate('A source is required to add an event rule'));
90+
91+
$emptyStateMessage = TemplateString::create(
92+
$this->translate(
93+
'No event rules found. To add a new event rule, please {{#link}}configure a Source{{/link}} first.'
94+
),
95+
[
96+
'link' => (new ActionLink(null, Links::sourceAdd()))->setBaseTarget('_next')
97+
]
98+
);
99+
} else {
100+
$addButton->openInModal();
101+
}
83102

84103
$this->addContent(
85104
(new ObjectList($eventRules, new EventRuleRenderer()))
86-
->setItemLayoutClass(DetailedItemLayout::class)
105+
->setItemLayoutClass(DetailedItemLayout::class)
106+
->setEmptyStateMessage($emptyStateMessage)
87107
);
88108

89109
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
@@ -99,12 +119,20 @@ public function addAction(): void
99119
$this->setTitle($this->translate('Add Event Rule'));
100120

101121
$eventRuleForm = (new EventRuleForm())
102-
->populate(['id' => -1])
122+
->setIsNew()
103123
->setCsrfCounterMeasureId(Session::getSession()->getId())
124+
->setAvailableSources(
125+
Database::get()->fetchPairs(
126+
Source::on(Database::get())->columns(['id', 'name'])->assembleSelect()
127+
)
128+
)
104129
->setAction(Url::fromRequest()->getAbsoluteUrl())
105130
->on(Form::ON_SUBMIT, function ($form) {
106131
$this->getResponse()->setHeader('X-Icinga-Container', 'col2');
107-
$this->redirectNow(Links::eventRule(-1)->addParams(['name' => $form->getValue('name')]));
132+
$this->redirectNow(Links::eventRule(-1)->addParams([
133+
'name' => $form->getValue('name'),
134+
'source' => $form->getValue('source')
135+
]));
108136
})->handleRequest($this->getServerRequest());
109137

110138
$this->addContent($eventRuleForm);

0 commit comments

Comments
 (0)