diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index f52b59ad..652c5e45 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -7,20 +7,22 @@ use Icinga\Module\Notifications\Common\Auth; use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Common\Links; +use Icinga\Module\Notifications\Forms\EventRuleConfigForm; use Icinga\Module\Notifications\Forms\EventRuleForm; -use Icinga\Module\Notifications\Forms\SaveEventRuleForm; use Icinga\Module\Notifications\Model\Incident; -use Icinga\Module\Notifications\Model\ObjectExtraTag; use Icinga\Module\Notifications\Model\Rule; use Icinga\Module\Notifications\Web\Control\SearchBar\ExtraTagSuggestions; -use Icinga\Module\Notifications\Widget\EventRuleConfig; use Icinga\Web\Notification; use Icinga\Web\Session; +use ipl\Html\Attributes; use ipl\Html\Form; +use ipl\Html\FormElement\SubmitButtonElement; use ipl\Html\Html; +use ipl\Html\HtmlElement; use ipl\Stdlib\Filter; use ipl\Web\Compat\CompatController; use ipl\Web\Control\SearchEditor; +use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; @@ -35,78 +37,118 @@ class EventRuleController extends CompatController public function init() { $this->sessionNamespace = Session::getSession()->getNamespace('notifications'); + $this->assertPermission('notifications/config/event-rule'); } public function indexAction(): void { - $this->assertPermission('notifications/config/event-rules'); + $this->sessionNamespace->delete('-1'); $this->addTitleTab(t('Event Rule')); - $this->controls->addAttributes(['class' => 'event-rule-detail']); $ruleId = $this->params->getRequired('id'); + $configValues = $this->sessionNamespace->get($ruleId); + $this->controls->addAttributes(['class' => 'event-rule-detail']); - $cache = $this->sessionNamespace->get($ruleId); + $disableSave = false; + if ($configValues === null) { + $configValues = $this->fromDb((int) $ruleId); + $disableSave = true; + } + + $eventRuleConfig = new EventRuleConfigForm( + $configValues, + Url::fromPath('notifications/event-rule/search-editor', ['id' => $ruleId]) + ); - if ($cache) { + $eventRuleConfig + ->populate($configValues) + ->on(Form::ON_SUCCESS, function (EventRuleConfigForm $form) use ($ruleId, $configValues) { + $form->addOrUpdateRule((int) $ruleId, $configValues); + $this->sessionNamespace->delete($ruleId); + Notification::success((sprintf(t('Successfully saved event rule %s'), $configValues['name']))); + $this->redirectNow(Links::eventRule((int) $ruleId)); + }) + ->on(EventRuleConfigForm::ON_DELETE, function (EventRuleConfigForm $form) use ($ruleId, $configValues) { + $form->removeRule((int) $ruleId); + $this->sessionNamespace->delete($ruleId); + Notification::success(sprintf(t('Successfully deleted event rule %s'), $configValues['name'])); + $this->redirectNow(Links::eventRules()); + }) + ->on(EventRuleConfigForm::ON_DISCARD, function () use ($ruleId, $configValues) { + $this->sessionNamespace->delete($ruleId); + Notification::success( + sprintf( + t('Successfully discarded changes to event rule %s'), + $configValues['name'] + ) + ); + $this->redirectNow(Links::eventRule((int) $ruleId)); + }) + ->on(EventRuleConfigForm::ON_CHANGE, function (EventRuleConfigForm $form) use ($ruleId, $configValues) { + $formValues = $form->getValues(); + $configValues = array_merge($configValues, $formValues); + $configValues['rule_escalation'] = $formValues['rule_escalation']; + $this->sessionNamespace->set($ruleId, $configValues); + }) + ->handleRequest($this->getServerRequest()); + + $cache = $this->sessionNamespace->get($ruleId); + $discardChangesButton = null; + if ($cache !== null) { $this->addContent(Html::tag('div', ['class' => 'cache-notice'], t('There are unsaved changes.'))); - $eventRuleConfig = new EventRuleConfig( - Url::fromPath('notifications/event-rule/search-editor', ['id' => $ruleId]), - $cache - ); - } else { - $eventRuleConfig = new EventRuleConfig( - Url::fromPath('notifications/event-rule/search-editor', ['id' => $ruleId]), - $this->fromDb($ruleId) + $discardChangesButton = new SubmitButtonElement( + 'discard_changes', + [ + 'label' => t('Discard Changes'), + 'form' => 'event-rule-config-form', + 'class' => 'btn-discard-changes', + 'formnovalidate' => true, + ] ); + + $disableSave = false; } - $disableRemoveButton = false; - if (ctype_digit($ruleId)) { - $incidents = Incident::on(Database::get()) + $buttonsWrapper = new HtmlElement('div', Attributes::create(['class' => ['icinga-controls', 'save-config']])); + $eventRuleConfigSubmitButton = new SubmitButtonElement( + 'save', + [ + 'label' => t('Save'), + 'form' => 'event-rule-config-form', + 'disabled' => $disableSave + ] + ); + + $deleteButton = new SubmitButtonElement( + 'delete', + [ + 'label' => t('Delete'), + 'form' => 'event-rule-config-form', + 'class' => 'btn-remove', + 'formnovalidate' => true + ] + ); + + $buttonsWrapper->add([$eventRuleConfigSubmitButton, $discardChangesButton, $deleteButton]); + + if ($ruleId > 0) { + $incidentCount = Incident::on(Database::get()) ->with('rule') - ->filter(Filter::equal('rule.id', $ruleId)); - - if ($incidents->count() > 0) { - $disableRemoveButton = true; + ->filter(Filter::equal('rule.id', $ruleId)) + ->count(); + + if ($incidentCount) { + $deleteButton->addAttributes([ + 'disabled' => true, + 'title' => t('There are active incidents for this event rule and hence cannot be removed') + ]); } } - $saveForm = (new SaveEventRuleForm()) - ->setShowRemoveButton() - ->setShowDismissChangesButton($cache !== null) - ->setRemoveButtonDisabled($disableRemoveButton) - ->setSubmitButtonDisabled($cache === null) - ->setSubmitLabel($this->translate('Save Changes')) - ->on(SaveEventRuleForm::ON_SUCCESS, function ($form) use ($ruleId, $eventRuleConfig) { - if ($form->getPressedSubmitElement()->getName() === 'discard_changes') { - $this->sessionNamespace->delete($ruleId); - Notification::success($this->translate('Successfully discarded the pending changes.')); - $this->redirectNow(Links::eventRule($ruleId)); - } - - if (! $eventRuleConfig->isValid()) { - $eventRuleConfig->addAttributes(['class' => 'invalid']); - return; - } - - $form->editRule($ruleId, $this->sessionNamespace->get($ruleId)); - $this->sessionNamespace->delete($ruleId); - - Notification::success($this->translate('Successfully updated rule.')); - $this->sendExtraUpdates(['#col1']); - $this->redirectNow(Links::eventRule($ruleId)); - })->on(SaveEventRuleForm::ON_REMOVE, function ($form) use ($ruleId) { - $form->removeRule($ruleId); - $this->sessionNamespace->delete($ruleId); - - Notification::success($this->translate('Successfully removed rule.')); - $this->redirectNow('__CLOSE__'); - })->handleRequest($this->getServerRequest()); - $eventRuleForm = Html::tag('div', ['class' => 'event-rule-form'], [ - Html::tag('h2', $eventRuleConfig->getConfig()['name'] ?? ''), + Html::tag('h2', $configValues['name']), (new Link( new Icon('edit'), Url::fromPath('notifications/event-rule/edit', [ @@ -116,29 +158,8 @@ public function indexAction(): void ))->openInModal() ]); - $eventRuleFormAndSave = Html::tag('div', ['class' => 'event-rule-and-save-forms']); - $eventRuleFormAndSave->add([ - $eventRuleForm, - $saveForm - ]); - - $eventRuleConfig - ->on(EventRuleConfig::ON_CHANGE, function ($eventRuleConfig) use ($ruleId, $saveForm) { - $this->sessionNamespace->set($ruleId, $eventRuleConfig->getConfig()); - $saveForm->setSubmitButtonDisabled(false); - $this->redirectNow(Links::eventRule($ruleId)); - }); - - foreach ($eventRuleConfig->getForms() as $form) { - $form->handleRequest($this->getServerRequest()); - - if (! $form->hasBeenSent()) { - // Force validation of populated values in case we display an unsaved rule - $form->validatePartial(); - } - } - - $this->addControl($eventRuleFormAndSave); + $this->addControl($eventRuleForm); + $this->addControl($buttonsWrapper); $this->addContent($eventRuleConfig); } @@ -146,6 +167,7 @@ public function indexAction(): void * Create config from db * * @param int $ruleId + * * @return array */ public function fromDb(int $ruleId): array @@ -163,16 +185,26 @@ public function fromDb(int $ruleId): array foreach ($rule->rule_escalation as $re) { foreach ($re as $k => $v) { - $config[$re->getTableName()][$re->position][$k] = $v; + if (in_array($k, ['id', 'condition'])) { + $config[$re->getTableName()][$re->position][$k] = (string) $v; + } } foreach ($re->rule_escalation_recipient as $recipient) { - $config[$re->getTableName()][$re->position]['recipient'][] = iterator_to_array($recipient); + $requiredValues = []; + + foreach ($recipient as $k => $v) { + if ($v !== null && in_array($k, ['contact_id', 'contactgroup_id', 'schedule_id'])) { + $requiredValues[$k] = (string) $v; + } elseif (in_array($k, ['id', 'channel_id'])) { + $requiredValues[$k] = $v ? (string) $v : null; + } + } + + $config[$re->getTableName()][$re->position]['recipients'][] = $requiredValues; } } - $config['showSearchbar'] = ! empty($config['object_filter']); - return $config; } @@ -188,7 +220,6 @@ public function completeAction(): void $this->getDocument()->add($suggestions); } - /** * searchEditorAction for Object Extra Tags * @@ -198,16 +229,26 @@ public function completeAction(): void */ public function searchEditorAction(): void { + /** @var string $ruleId */ $ruleId = $this->params->shiftRequired('id'); - $eventRule = $this->sessionNamespace->get($ruleId) ?? $this->fromDb($ruleId); + $eventRule = $this->sessionNamespace->get($ruleId); - $editor = EventRuleConfig::createSearchEditor() - ->setQueryString($eventRule['object_filter'] ?? ''); + if ($eventRule === null) { + $eventRule = $this->fromDb((int) $ruleId); + } - $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($ruleId, $eventRule) { - $eventRule['object_filter'] = EventRuleConfig::createFilterString($form->getFilter()); + $editor = new SearchEditor(); + $objectFilter = $eventRule['object_filter'] ?? ''; + $editor->setQueryString($objectFilter) + ->setAction(Url::fromRequest()->getAbsoluteUrl()) + ->setSuggestionUrl( + Links::ruleFilterSuggestionUrl($ruleId)->addParams(['_disableLayout' => true, 'showCompact' => true]) + ); + + $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($ruleId, $eventRule) { + $eventRule['object_filter'] = self::createFilterString($form->getFilter()); $this->sessionNamespace->set($ruleId, $eventRule); $this->getResponse() ->setHeader('X-Icinga-Container', '_self') @@ -221,52 +262,60 @@ public function searchEditorAction(): void $editor->handleRequest($this->getServerRequest()); - $this->getDocument()->add($editor); + $this->getDocument()->addHtml($editor); $this->setTitle($this->translate('Adjust Filter')); } + /** + * Create filter string from the given filter rule + * + * @param Filter\Rule $filters + * + * @return ?string + */ + public static function createFilterString(Filter\Rule $filters): ?string + { + if ($filters instanceof Filter\Chain) { + foreach ($filters as $filter) { + /** @var Filter\Condition $filter */ + $filter->setValue(true); + } + } elseif ($filters instanceof Filter\Condition && empty($filters->getValue())) { + $filters->setValue(true); + } + + $filterStr = QueryString::render($filters); + + return $filterStr !== '' ? rawurldecode($filterStr) : null; + } + public function editAction(): void { /** @var string $ruleId */ $ruleId = $this->params->getRequired('id'); - /** @var ?array $cache */ - $cache = $this->sessionNamespace->get($ruleId); - - if ($this->params->has('clearCache')) { - $this->sessionNamespace->delete($ruleId); - $cache = []; - } - - if (isset($cache) || $ruleId === '-1') { - $config = $cache ?? []; - } else { - $config = $this->fromDb((int) $ruleId); + $config = $this->sessionNamespace->get($ruleId); + if ($config === null) { + if ($ruleId === '-1') { + $config = ['id' => $ruleId]; + } else { + $config = $this->fromDb((int) $ruleId); + } } $eventRuleForm = (new EventRuleForm()) ->populate($config) ->setAction(Url::fromRequest()->getAbsoluteUrl()) - ->on(Form::ON_SUCCESS, function ($form) use ($ruleId, $cache, $config) { + ->on(Form::ON_SUCCESS, function ($form) use ($ruleId, $config) { $config['name'] = $form->getValue('name'); $config['is_active'] = $form->getValue('is_active'); - - if ($cache || $ruleId === '-1') { - $this->sessionNamespace->set($ruleId, $config); - } else { - (new SaveEventRuleForm())->editRule((int) $ruleId, $config); - } - if ($ruleId === '-1') { - $redirectUrl = Url::fromPath('notifications/event-rules/add', [ - 'use_cache' => true - ]); + $redirectUrl = Url::fromPath('notifications/event-rules/add', ['id' => '-1']); } else { - $redirectUrl = Url::fromPath('notifications/event-rule', [ - 'id' => $ruleId - ]); + $redirectUrl = Url::fromPath('notifications/event-rule', ['id' => $ruleId]); $this->sendExtraUpdates(['#col1']); } + $this->sessionNamespace->set($ruleId, $config); $this->getResponse()->setHeader('X-Icinga-Container', 'col2'); $this->redirectNow($redirectUrl); })->handleRequest($this->getServerRequest()); diff --git a/application/controllers/EventRulesController.php b/application/controllers/EventRulesController.php index a04cbc55..a1a3fa78 100644 --- a/application/controllers/EventRulesController.php +++ b/application/controllers/EventRulesController.php @@ -6,20 +6,20 @@ use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Common\Links; -use Icinga\Module\Notifications\Forms\SaveEventRuleForm; +use Icinga\Module\Notifications\Forms\EventRuleConfigForm; use Icinga\Module\Notifications\Model\Rule; -use Icinga\Module\Notifications\Web\Control\SearchBar\ObjectSuggestions; -use Icinga\Module\Notifications\Widget\EventRuleConfig; use Icinga\Module\Notifications\Widget\ItemList\EventRuleList; use Icinga\Web\Notification; use Icinga\Web\Session; +use ipl\Html\Attributes; +use ipl\Html\Form; +use ipl\Html\FormElement\SubmitButtonElement; use ipl\Html\Html; +use ipl\Html\HtmlElement; use ipl\Stdlib\Filter; use ipl\Web\Compat\CompatController; use ipl\Web\Compat\SearchControls; -use ipl\Web\Control\LimitControl; use ipl\Web\Control\SearchEditor; -use ipl\Web\Control\SortControl; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\ButtonLink; @@ -45,6 +45,7 @@ public function init() public function indexAction(): void { $eventRules = Rule::on(Database::get()); + $this->sessionNamespace->delete('-1'); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($eventRules); @@ -84,7 +85,7 @@ public function indexAction(): void $this->addContent( (new ButtonLink( t('New Event Rule'), - Url::fromPath('notifications/event-rule/edit', ['id' => -1, 'clearCache' => true]), + Url::fromPath('notifications/event-rule/edit', ['id' => -1]), 'plus' ))->openInModal() ->addAttributes(['class' => 'new-event-rule']) @@ -103,22 +104,49 @@ public function indexAction(): void public function addAction(): void { $this->addTitleTab(t('Add Event Rule')); - $this->getTabs()->setRefreshUrl(Url::fromPath('notifications/event-rules/add')); + $this->getTabs()->setRefreshUrl(Url::fromPath('notifications/event-rules/add', ['id' => '-1'])); $this->controls->addAttributes(['class' => 'event-rule-detail']); + $ruleId = $this->params->get('id'); + $config = $this->sessionNamespace->get($ruleId); + $config['object_filter'] = $config['object_filter'] ?? null; - if ($this->params->has('use_cache') || $this->getServerRequest()->getMethod() !== 'GET') { - $cache = $this->sessionNamespace->get(-1, []); - } else { - $this->sessionNamespace->delete(-1); - - $cache = []; - } + $eventRuleConfigSubmitButton = (new SubmitButtonElement( + 'save', + [ + 'label' => t('Add Event Rule'), + 'form' => 'event-rule-config-form' + ] + ))->setWrapper(new HtmlElement('div', Attributes::create(['class' => ['icinga-controls', 'save-config']]))); + + $eventRuleConfig = new EventRuleConfigForm( + $config, + Url::fromPath( + 'notifications/event-rules/search-editor', + ['id' => $ruleId] + ) + ); - $eventRuleConfig = new EventRuleConfig(Url::fromPath('notifications/event-rules/add-search-editor'), $cache); + $eventRuleConfig + ->populate($config) + ->on(Form::ON_SUCCESS, function (EventRuleConfigForm $form) use ($config) { + $ruleId = (int) $config['id']; + $ruleName = $config['name']; + $insertId = $form->addOrUpdateRule($ruleId, $config); + $this->sessionNamespace->delete($ruleId); + Notification::success(sprintf(t('Successfully add event rule %s'), $ruleName)); + $this->redirectNow(Links::eventRule($insertId)); + }) + ->on(EventRuleConfigForm::ON_CHANGE, function (EventRuleConfigForm $form) use ($config) { + $formValues = $form->getValues(); + $config = array_merge($config, $formValues); + $config['rule_escalation'] = $formValues['rule_escalation']; + $this->sessionNamespace->set('-1', $config); + }) + ->handleRequest($this->getServerRequest()); $eventRuleForm = Html::tag('div', ['class' => 'event-rule-form'], [ - Html::tag('h2', $eventRuleConfig->getConfig()['name'] ?? ''), + Html::tag('h2', $config['name'] ?? ''), (new Link( new Icon('edit'), Url::fromPath('notifications/event-rule/edit', [ @@ -128,86 +156,37 @@ public function addAction(): void ))->openInModal() ]); - $saveForm = (new SaveEventRuleForm()) - ->on(SaveEventRuleForm::ON_SUCCESS, function ($saveForm) use ($eventRuleConfig) { - if (! $eventRuleConfig->isValid()) { - $eventRuleConfig->addAttributes(['class' => 'invalid']); - return; - } - - $id = $saveForm->addRule($this->sessionNamespace->get(-1)); - - Notification::success($this->translate('Successfully added rule.')); - $this->sendExtraUpdates(['#col1']); - $this->redirectNow(Links::eventRule($id)); - })->handleRequest($this->getServerRequest()); - - $eventRuleConfig->on(EventRuleConfig::ON_CHANGE, function ($eventRuleConfig) { - $this->sessionNamespace->set(-1, $eventRuleConfig->getConfig()); - - $this->redirectNow(Url::fromPath('notifications/event-rules/add', ['use_cache' => true])); - }); - - foreach ($eventRuleConfig->getForms() as $f) { - $f->handleRequest($this->getServerRequest()); - - if (! $f->hasBeenSent()) { - // Force validation of populated values in case we display an unsaved rule - $f->validatePartial(); - } - } - - $eventRuleFormAndSave = Html::tag('div', ['class' => 'event-rule-and-save-forms']); - $eventRuleFormAndSave->add([ - $eventRuleForm, - $saveForm - ]); - - $this->addControl($eventRuleFormAndSave); + $this->addControl($eventRuleForm); + $this->addControl($eventRuleConfigSubmitButton); $this->addContent($eventRuleConfig); } - public function completeAction(): void - { - $suggestions = new ObjectSuggestions(); - $suggestions->setModel(Rule::class); - $suggestions->forRequest($this->getServerRequest()); - $this->getDocument()->add($suggestions); - } - public function searchEditorAction(): void { - $editor = $this->createSearchEditor( - Rule::on(Database::get()), - [ - LimitControl::DEFAULT_LIMIT_PARAM, - SortControl::DEFAULT_SORT_PARAM, - ] - ); - - $this->getDocument()->add($editor); - $this->setTitle($this->translate('Adjust Filter')); - } + $ruleId = $this->params->shiftRequired('id'); + $eventRule = $this->sessionNamespace->get($ruleId); - public function addSearchEditorAction(): void - { - $cache = $this->sessionNamespace->get(-1); + if ($eventRule === null) { + $eventRule = ['id' => '-1']; + } - $editor = EventRuleConfig::createSearchEditor() - ->setQueryString($cache['object_filter'] ?? ''); + $editor = new SearchEditor(); - $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) { - $cache = $this->sessionNamespace->get(-1); - $cache['object_filter'] = EventRuleConfig::createFilterString($form->getFilter()); + $objectFilter = $eventRule['object_filter'] ?? ''; + $editor->setQueryString($objectFilter) + ->setAction(Url::fromRequest()->getAbsoluteUrl()) + ->setSuggestionUrl(Links::ruleFilterSuggestionUrl($ruleId)); - $this->sessionNamespace->set(-1, $cache); + $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($ruleId, $eventRule) { + $eventRule['object_filter'] = self::createFilterString($form->getFilter()); + $this->sessionNamespace->set($ruleId, $eventRule); $this->getResponse() ->setHeader('X-Icinga-Container', '_self') ->redirectAndExit( Url::fromPath( 'notifications/event-rules/add', - ['use_cache' => true] + ['id' => $ruleId] ) ); }); @@ -218,6 +197,28 @@ public function addSearchEditorAction(): void $this->setTitle($this->translate('Adjust Filter')); } + /** + * Create filter string from the given filter rule + * + * @param Filter\Rule $filters + * + * @return ?string + */ + public static function createFilterString(Filter\Rule $filters): ?string + { + if ($filters instanceof Filter\Chain) { + foreach ($filters as $filter) { + self::createFilterString($filter); + } + } elseif ($filters instanceof Filter\Condition && empty($filters->getValue())) { + $filters->setValue(true); + } + + $filterStr = QueryString::render($filters); + + return $filterStr !== '' ? rawurldecode($filterStr) : null; + } + /** * Get the filter created from query string parameters * diff --git a/application/forms/AddEscalationForm.php b/application/forms/AddEscalationForm.php deleted file mode 100644 index 44e2ba6c..00000000 --- a/application/forms/AddEscalationForm.php +++ /dev/null @@ -1,42 +0,0 @@ - ['add-escalation-form', 'icinga-form', 'icinga-controls'], - 'name' => 'add-escalation-form' - ]; - - - protected function assemble() - { - $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); - $this->add($this->createUidElement()); - - - $this->addElement( - 'submitButton', - 'add', - [ - 'class' => ['add-button', 'control-button', 'spinner'], - 'label' => new Icon('plus'), - 'title' => $this->translate('Add a new escalation') - ] - ); - } -} diff --git a/application/forms/AddFilterForm.php b/application/forms/AddFilterForm.php deleted file mode 100644 index aee6ded6..00000000 --- a/application/forms/AddFilterForm.php +++ /dev/null @@ -1,42 +0,0 @@ - ['add-filter-form', 'icinga-form', 'icinga-controls'], - 'name' => 'add-filter-form' - ]; - - - protected function assemble() - { - $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); - $this->add($this->createUidElement()); - - - $this->addElement( - 'submitButton', - 'add', - [ - 'class' => ['add-button', 'control-button', 'spinner'], - 'label' => new Icon('plus'), - 'title' => $this->translate('Add filter') - ] - ); - } -} diff --git a/application/forms/BaseEscalationForm.php b/application/forms/BaseEscalationForm.php deleted file mode 100644 index d4dab933..00000000 --- a/application/forms/BaseEscalationForm.php +++ /dev/null @@ -1,89 +0,0 @@ - ['escalation-form', 'icinga-form', 'icinga-controls']]; - - /** @var int The count of existing conditions/recipients */ - protected $count; - - /** @var bool Whether the `add` button is pressed */ - protected $isAddPressed; - - /** @var ValidHtml[] */ - protected $options; - - /** @var ?int The counter of removed option */ - protected $removedOptionNumber; - - public function __construct(int $count) - { - $this->count = $count; - } - - public function hasBeenSubmitted() - { - return false; - } - - abstract protected function assembleElements(): void; - - protected function createAddButton(): FormElement - { - $addButton = $this->createElement( - 'submitButton', - 'add', - [ - 'class' => ['add-button', 'control-button', 'spinner'], - 'label' => new Icon('plus'), - 'title' => $this->translate('Add more'), - 'formnovalidate' => true - ] - ); - - $this->registerElement($addButton); - - return $addButton; - } - - protected function assemble() - { - $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); - $this->add($this->createUidElement()); - - $addButton = $this->createAddButton(); - - $button = $this->getPressedSubmitElement(); - if ($button && $button->getName() === 'add') { - $this->isAddPressed = true; - } - - if ($this->count || $this->isAddPressed) { - $this->assembleElements(); - } - - $this->add($addButton); - } - - public function isAddButtonPressed(): ?bool - { - return $this->isAddPressed; - } -} diff --git a/application/forms/EscalationConditionForm.php b/application/forms/EscalationConditionForm.php deleted file mode 100644 index 96b6b51d..00000000 --- a/application/forms/EscalationConditionForm.php +++ /dev/null @@ -1,286 +0,0 @@ -addAttributes(['class' => 'escalation-condition-form']); - - parent::__construct($count ?? 0); - } - - protected function assembleElements(): void - { - $end = $this->count; - if ($this->isAddPressed) { - $end++; - } - - foreach (range(1, $end) as $count) { - $col = $this->createElement( - 'select', - 'column' . $count, - [ - 'class' => ['autosubmit', 'left-operand'], - 'options' => [ - '' => sprintf(' - %s - ', $this->translate('Please choose')), - 'incident_severity' => $this->translate('Incident Severity'), - 'incident_age' => $this->translate('Incident Age') - ], - 'disabledOptions' => [''], - 'required' => true - ] - ); - - $operators = ['=', '>', '>=', '<', '<=', '!=']; - $op = $this->createElement( - 'select', - 'operator' . $count, - [ - 'class' => ['class' => 'operator-input', 'autosubmit'], - 'options' => array_combine($operators, $operators), - 'required' => true - ] - ); - - switch ($this->getPopulatedValue('column' . $count)) { - case 'incident_severity': - $val = $this->createElement( - 'select', - 'value' . $count, - [ - 'class' => ['autosubmit', 'right-operand'], - 'options' => [ - 'ok' => $this->translate('Ok', 'notification.severity'), - 'debug' => $this->translate('Debug', 'notification.severity'), - 'info' => $this->translate('Information', 'notification.severity'), - 'notice' => $this->translate('Notice', 'notification.severity'), - 'warning' => $this->translate('Warning', 'notification.severity'), - 'err' => $this->translate('Error', 'notification.severity'), - 'crit' => $this->translate('Critical', 'notification.severity'), - 'alert' => $this->translate('Alert', 'notification.severity'), - 'emerg' => $this->translate('Emergency', 'notification.severity') - ] - ] - ); - - if ( - $this->getPopulatedValue('type' . $count) !== 'incident_severity' - && $this->getPopulatedValue('type' . $count) !== null - ) { - $this->clearPopulatedValue('type' . $count); - $this->clearPopulatedValue('value' . $count); - } - - $this->addElement('hidden', 'type' . $count, [ - 'ignore' => true, - 'value' => 'incident_severity' - ]); - - break; - case 'incident_age': - $val = $this->createElement( - 'text', - 'value' . $count, - [ - 'required' => true, - 'class' => ['autosubmit', 'right-operand'], - 'validators' => [new CallbackValidator(function ($value, $validator) { - if (! preg_match('~^\d+(?:\.?\d*)?[hms]{1}$~', $value)) { - $validator->addMessage($this->translate( - 'Only numbers with optional fractions (separated by a dot)' - . ' and one of these suffixes are allowed: h, m, s' - )); - - return false; - } - - return true; - })] - ] - ); - - if ( - $this->getPopulatedValue('type' . $count) !== 'incident_age' - && $this->getPopulatedValue('type' . $count) !== null - ) { - $this->clearPopulatedValue('type' . $count); - $this->clearPopulatedValue('value' . $count); - } - - $this->addElement('hidden', 'type' . $count, [ - 'ignore' => true, - 'value' => 'incident_age' - ]); - - break; - default: - $val = $this->createElement('text', 'value' . $count, [ - 'class' => 'right-operand', - 'placeholder' => $this->translate('Please make a decision'), - 'disabled' => true - ]); - } - - $this->registerElement($col); - $this->registerElement($op); - $this->registerElement($val); - - (new EventRuleDecorator())->decorate($val); - - $this->options[$count] = Html::tag( - 'li', - ['class' => 'option'], - [$col, $op, $val, $this->createRemoveButton($count)] - ); - } - - $this->handleRemove(); - - $this->add(Html::tag('ul', ['class' => 'options'], $this->options)); - } - - public function getValues() - { - $filter = Filter::any(); - - if ($this->count > 0) { // if count is 0, loop runs in reverse direction - foreach (range(1, $this->count) as $count) { - if ($this->removedOptionNumber === $count) { - continue; // removed option - } - - $chosenType = $this->getValue('column' . $count, 'placeholder'); - - $filterStr = $chosenType - . $this->getValue('operator' . $count) - . ($this->getValue('value' . $count) ?? ($chosenType === 'incident_severity' ? 'ok' : '')); - - $filter->add(QueryString::parse($filterStr)); - } - } - - if ($this->isAddPressed) { - $filter->add(QueryString::parse('placeholder=')); - } - - return (new FilterRenderer($filter)) - ->render(); - } - - public function populate($values) - { - foreach ($values as $key => $condition) { - if (! is_int($key)) { - // csrf token and uid - continue; - } - - $count = $key + 1; - if (empty($condition)) { // when other conditions are removed and only 1 pending with no values - $values['column' . $count] = null; - $values['operator' . $count] = null; - $values['value' . $count] = null; - - continue; - } - - $filter = QueryString::parse($condition); - - $values['column' . $count] = $filter->getColumn() === 'placeholder' ? null : $filter->getColumn(); - $values['operator' . $count] = QueryString::getRuleSymbol($filter); - $values['value' . $count] = $filter->getValue(); - } - - return parent::populate($values); - } - - protected function createRemoveButton(int $count): ?FormElement - { - if ($this->deleteRemoveButton && $this->count === 1 && ! $this->isAddPressed) { - return null; - } - - $removeButton = $this->createElement( - 'submitButton', - 'remove_' . $count, - [ - 'class' => ['remove-button', 'control-button', 'spinner'], - 'label' => new Icon('minus'), - 'title' => $this->translate('Remove'), - 'formnovalidate' => true - ] - ); - - $this->registerElement($removeButton); - - return $removeButton; - } - - protected function handleRemove(): void - { - $button = $this->getPressedSubmitElement(); - - if ($button && $button->getName() !== 'add') { - [$name, $toRemove] = explode('_', $button->getName(), 2); - $toRemove = (int) $toRemove; - $this->removedOptionNumber = $toRemove; - $optionCount = count($this->options); - - for ($i = $toRemove; $i < $optionCount; $i++) { - $nextCount = $i + 1; - $this->getElement('column' . $nextCount)->setName('column' . $i); - $this->getElement('operator' . $nextCount)->setName('operator' . $i); - $this->getElement('value' . $nextCount)->setName('value' . $i); - - $this->getElement('remove_' . $nextCount)->setName('remove_' . $i); - } - - unset($this->options[$toRemove]); - - if ($this->deleteRemoveButton && count($this->options) === 1) { - $key = array_key_last($this->options); - $this->options[$key]->remove($this->getElement('remove_' . $key)); - } - } - - if (empty($this->options)) { - $this->addAttributes(['class' => 'count-zero-escalation-condition-form']); - } else { - $this->getAttributes() - ->remove('class', 'count-zero-escalation-condition-form'); - } - } - - /** - * Whether to delete the remove button - * - * @param bool $delete - * - * @return $this - */ - public function deleteRemoveButton(bool $delete = true): self - { - $this->deleteRemoveButton = $delete; - - return $this; - } -} diff --git a/application/forms/EscalationRecipientForm.php b/application/forms/EscalationRecipientForm.php deleted file mode 100644 index 006d71a8..00000000 --- a/application/forms/EscalationRecipientForm.php +++ /dev/null @@ -1,232 +0,0 @@ -addAttributes(['class' => 'escalation-recipient-form']); - - parent::__construct($count ?? 1); - } - - protected function fetchOptions(): array - { - $options = []; - foreach (Contact::on(Database::get()) as $contact) { - $options['Contacts']['contact_' . $contact->id] = $contact->full_name; - } - - foreach (Contactgroup::on(Database::get()) as $contactgroup) { - $options['Contact Groups']['contactgroup_' . $contactgroup->id] = $contactgroup->name; - } - - foreach (Schedule::on(Database::get()) as $schedule) { - $options['Schedules']['schedule_' . $schedule->id] = $schedule->name; - } - - return $options; - } - - protected function assembleElements(): void - { - $end = $this->count; - if ($this->isAddPressed) { - $end++; - } - - foreach (range(1, $end) as $count) { - $escalationRecipientId = $this->createElement( - 'hidden', - 'id' . $count - ); - - $this->registerElement($escalationRecipientId); - - $col = $this->createElement( - 'select', - 'column' . $count, - [ - 'class' => ['autosubmit', 'left-operand'], - 'options' => [ - '' => sprintf(' - %s - ', $this->translate('Please choose')) - ] + $this->fetchOptions(), - 'disabledOptions' => [''], - 'required' => true - ] - ); - - $this->registerElement($col); - - $options = ['' => sprintf(' - %s - ', $this->translate('Please choose'))]; - $options += Channel::fetchChannelNames(Database::get()); - - $val = $this->createElement( - 'select', - 'value' . $count, - [ - 'class' => ['autosubmit', 'right-operand'], - 'options' => $options, - 'disabledOptions' => [''] - ] - ); - - if ($this->getValue('column' . $count) !== null) { - $recipient = explode('_', $this->getValue('column' . $count)); - if ($recipient[0] === 'contact') { - $options[''] = $this->translate('Default User Channel'); - - $val->setOptions($options); - - $val->setDisabledOptions([]); - - if ($this->getPopulatedValue('value' . $count, '') === '') { - $val->addAttributes(['class' => 'default-channel']); - } - } - } else { - $val = $this->createElement('text', 'value' . $count, [ - 'class' => 'right-operand', - 'placeholder' => $this->translate('Please make a decision'), - 'disabled' => true - ]); - } - - $this->registerElement($val); - - $this->options[$count] = Html::tag( - 'li', - ['class' => 'option'], - [$col, $val, $this->createRemoveButton($count)] - ); - } - - $this->handleRemove(); - - $this->add(Html::tag('ul', ['class' => 'options'], $this->options)); - } - - public function getValues() - { - $end = $this->count; - if ($this->isAddPressed) { - $end++; - } - - $values = []; - foreach (range(1, $end) as $count) { - if ($this->removedOptionNumber === $count) { - continue; // removed option - } - - $value = []; - $value['channel_id'] = $this->getValue('value' . $count); - $value['id'] = $this->getValue('id' . $count); - - $columnName = $this->getValue('column' . $count); - - if ($columnName === null) { - $values[] = $value; - continue; - } - - [$columnName, $id] = explode('_', $columnName, 2); - - $value[$columnName . '_id'] = $id; - - $values[] = $value; - } - - return $values; - } - - public function populate($values) - { - /** @var int $key */ - foreach ($values as $key => $condition) { - if (is_array($condition)) { - $count = 0; - foreach ($condition as $elementName => $elementValue) { - if ($elementValue === null) { - continue; - } - - $count = $key + 1; - $selectedOption = str_replace('id', $elementValue, $elementName, $replaced); - if ($replaced && $elementName !== 'channel_id') { - $values['column' . $count] = $selectedOption; - } elseif ($elementName === 'channel_id') { - $values['value' . $count] = $elementValue; - } - } - - if (isset($condition['id'])) { - $values['id' . $count] = $condition['id']; - } - } - } - - return parent::populate($values); - } - - protected function createRemoveButton(int $count): ?FormElement - { - if ($this->count === 1 && ! $this->isAddPressed) { - return null; - } - - $removeButton = $this->createElement( - 'submitButton', - 'remove_' . $count, - [ - 'class' => ['remove-button', 'control-button', 'spinner'], - 'label' => new Icon('minus'), - 'title' => $this->translate('Remove'), - 'formnovalidate' => true - ] - ); - - $this->registerElement($removeButton); - - return $removeButton; - } - - protected function handleRemove(): void - { - $button = $this->getPressedSubmitElement(); - - if ($button && $button->getName() !== 'add') { - [$name, $toRemove] = explode('_', $button->getName(), 2); - $toRemove = (int) $toRemove; - $this->removedOptionNumber = $toRemove; - $optionCount = count($this->options); - - for ($i = $toRemove; $i < $optionCount; $i++) { - $nextCount = $i + 1; - $this->getElement('column' . $nextCount)->setName('column' . $i); - $this->getElement('value' . $nextCount)->setName('value' . $i); - - $this->getElement('remove_' . $nextCount)->setName('remove_' . $i); - } - - unset($this->options[$toRemove]); - - if (count($this->options) === 1) { - $key = array_key_last($this->options); - $this->options[$key]->remove($this->getElement('remove_' . $key)); - } - } - } -} diff --git a/application/forms/EventRuleConfigElements/EscalationCondition.php b/application/forms/EventRuleConfigElements/EscalationCondition.php new file mode 100644 index 00000000..f9ae52b4 --- /dev/null +++ b/application/forms/EventRuleConfigElements/EscalationCondition.php @@ -0,0 +1,269 @@ + 'escalation-condition']; + + /** @var EscalationConditionListItem[] Condition list items */ + protected $conditions = []; + + /** @var EventRuleConfigForm */ + protected $configForm; + + /** @var string */ + protected $prefix; + + public function __construct(string $prefix, EventRuleConfigForm $configForm) + { + $this->prefix = $prefix; + $this->configForm = $configForm; + + parent::__construct('escalation-condition_' . $this->prefix); + } + + protected function assemble(): void + { + $this->addElement('hidden', 'condition-count'); + // Escalation Id to which the condition belongs + $this->addElement('hidden', 'id'); + + $addCondition = $this->createElement( + 'submitButton', + 'add-condition', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'title' => $this->translate('Add Condition'), + 'formnovalidate' => true + ] + ); + + $this->registerElement($addCondition); + + $conditionCount = $this->getValue('condition-count'); + $zeroConditions = $this->configForm->getValue('zero-condition-escalation') === $this->prefix; + $defaultCount = 1; + $configHasZeroConditionEscalation = $this->configForm->hasZeroConditionEscalation(); + if ($zeroConditions && $configHasZeroConditionEscalation) { + $defaultCount = 0; + $conditionCount = $defaultCount; + } else { + $conditionCount = $conditionCount === null ? $defaultCount : (int) $conditionCount; + } + + if ($addCondition->hasBeenPressed()) { + ++$conditionCount; + if ($defaultCount === 0 && $conditionCount === 1) { + $configHasZeroConditionEscalation = false; + } + } + + $this->getElement('condition-count')->setValue($conditionCount); + if ($conditionCount === 0) { + $this->addAttributes(['class' => 'zero-escalation-condition']); + $this->addElement($addCondition); + + return; + } + + $this->getAttributes()->remove('class', 'zero-escalation-condition'); + $removePosition = null; + + for ($i = 1; $i <= $conditionCount; $i++) { + $col = $this->createElement( + 'select', + 'column_' . $i, + [ + 'class' => ['autosubmit', 'left-operand'], + 'options' => [ + '' => sprintf(' - %s - ', $this->translate('Please choose')), + 'incident_severity' => $this->translate('Incident Severity'), + 'incident_age' => $this->translate('Incident Age') + ], + 'disabledOptions' => [''], + 'required' => true + ] + ); + + $operators = ['=', '>', '>=', '<', '<=', '!=']; + $op = $this->createElement( + 'select', + 'operator_' . $i, + [ + 'class' => ['operator-input', 'autosubmit'], + 'options' => array_combine($operators, $operators), + 'required' => true + ] + ); + + $valName = 'val_' . $i; + switch ($this->getPopulatedValue('column_' . $i)) { + case 'incident_severity': + $val = $this->createElement( + 'select', + $valName, + [ + 'class' => ['autosubmit', 'right-operand'], + 'options' => [ + 'ok' => $this->translate('Ok', 'notification.severity'), + 'debug' => $this->translate('Debug', 'notification.severity'), + 'info' => $this->translate('Information', 'notification.severity'), + 'notice' => $this->translate('Notice', 'notification.severity'), + 'warning' => $this->translate('Warning', 'notification.severity'), + 'err' => $this->translate('Error', 'notification.severity'), + 'crit' => $this->translate('Critical', 'notification.severity'), + 'alert' => $this->translate('Alert', 'notification.severity'), + 'emerg' => $this->translate('Emergency', 'notification.severity') + ] + ] + ); + + break; + case 'incident_age': + $val = $this->createElement( + 'text', + $valName, + [ + 'required' => true, + 'class' => ['autosubmit', 'right-operand'], + 'validators' => [ + new CallbackValidator(function ($value, $validator) { + if (! preg_match('~^\d+(?:\.?\d*)?[hms]{1}$~', $value)) { + $validator->addMessage( + $this->translate( + 'Only numbers with optional fractions (separated by a dot)' + . ' and one of these suffixes are allowed: h, m, s' + ) + ); + + return false; + } + + $validator->clearMessages(); + + return true; + }) + ] + ] + ); + + break; + default: + $val = $this->createElement('text', $valName, [ + 'class' => 'right-operand', + 'placeholder' => $this->translate('Please make a decision'), + 'disabled' => true + ]); + } + + $this->registerElement($col); + $this->registerElement($op); + $this->registerElement($val); + + $removeButton = null; + + if (($conditionCount > 1) || ($conditionCount === 1 && ! $configHasZeroConditionEscalation)) { + $removeButton = $this->createRemoveButton($i); + if ($removeButton->hasBeenPressed()) { + $removePosition = $i; + } + } + + (new EventRuleDecorator())->decorate($val); + $this->conditions[$i] = new EscalationConditionListItem($i, $col, $op, $val, $removeButton); + } + + if ($removePosition) { + $this->getElement('condition-count')->setValue(--$conditionCount); + if ($conditionCount === 1 && $configHasZeroConditionEscalation) { + $idx = $removePosition === 1 ? 2 : 1; + $this->conditions[$idx]->setRemoveButton(null); + } + } + + $this->add(new EscalationConditionList($this->conditions)); + $this->addElement($addCondition); + } + + /** + * Create remove button for the condition in the given position + * + * @param int $count + * + * @return SubmitButtonElement + */ + protected function createRemoveButton(int $count): SubmitButtonElement + { + $removeButton = new SubmitButtonElement( + 'remove', + [ + 'class' => ['remove-button', 'control-button', 'spinner'], + 'label' => new Icon('minus'), + 'title' => $this->translate('Remove'), + 'formnovalidate' => true, + 'value' => (string) $count + ] + ); + + $this->registerElement($removeButton); + + return $removeButton; + } + + public function hasValue(): bool + { + $this->ensureAssembled(); + + return parent::hasValue(); + } + + /** + * Get the rendered condition + * + * @return string + */ + public function getCondition(): string + { + $count = (int) $this->getValue('condition-count'); + if ($count === 0) { + return ''; + } + + $filter = Filter::any(); + $removePosition = (int) $this->getValue('remove'); + if ($removePosition) { + $count += 1; + } + + foreach (range(1, $count) as $count) { + if ($count === $removePosition) { + continue; + } + + $chosenType = $this->getValue('column_' . $count, 'placeholder'); + + $filterStr = $chosenType + . $this->getValue('operator_' . $count) + . ($this->getValue('val_' . $count) ?? ($chosenType === 'incident_severity' ? 'ok' : '')); + + $filter->add(QueryString::parse($filterStr)); + } + + return (new FilterRenderer($filter)) + ->render(); + } +} diff --git a/application/forms/EventRuleConfigElements/EscalationRecipient.php b/application/forms/EventRuleConfigElements/EscalationRecipient.php new file mode 100644 index 00000000..0900cec1 --- /dev/null +++ b/application/forms/EventRuleConfigElements/EscalationRecipient.php @@ -0,0 +1,228 @@ + 'escalation-recipient']; + + /** @var EscalationRecipientListItem[] */ + protected $recipients = []; + + public function __construct($name) + { + parent::__construct('escalation-recipient_' . $name, []); + } + + protected function assemble(): void + { + $this->addElement('hidden', 'recipient-count', ['value' => '1']); + + $addRecipientButton = $this->createElement( + 'submitButton', + 'add-recipient', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'title' => $this->translate('Add Recipient'), + 'formnovalidate' => true + ] + ); + + $this->registerElement($addRecipientButton); + $recipientCount = (int) $this->getValue('recipient-count'); + if ($addRecipientButton->hasBeenPressed()) { + $this->getElement('recipient-count')->setValue(++$recipientCount); + } + + $defaultOption = ['' => sprintf(' - %s - ', $this->translate('Please choose'))]; + $removePosition = null; + + foreach (range(1, $recipientCount) as $i) { + $this->addElement('hidden', 'id_' . $i); + + $col = $this->createElement( + 'select', + 'column_' . $i, + [ + 'class' => ['autosubmit', 'left-operand'], + 'options' => $defaultOption + $this->fetchOptions(), + 'disabledOptions' => [''], + 'required' => true, + 'value' => $this->getPopulatedValue('column_' . $i) + ] + ); + + $this->registerElement($col); + + $options = $defaultOption + Channel::fetchChannelNames(Database::get()); + + $val = $this->createElement( + 'select', + 'val_' . $i, + [ + 'class' => ['autosubmit', 'right-operand'], + 'options' => $options, + 'disabledOptions' => [''], + 'value' => $this->getPopulatedValue('val_' . $i) + ] + ); + + $recipientVal = $this->getValue('column_' . $i); + if ($recipientVal !== null) { + $recipient = explode('_', $recipientVal); + if ($recipient[0] === 'contact') { + $options[''] = $this->translate('Default User Channel'); + + $val->setOptions($options); + $val->setDisabledOptions([]); + + if ($this->getPopulatedValue('val_' . $i, '') === '') { + $val->addAttributes(['class' => 'default-channel']); + } + } else { + $val->addAttributes(['required' => true]); + } + } else { + $val = $this->createElement('text', 'val_' . $i, [ + 'class' => 'right-operand', + 'placeholder' => $this->translate('Please make a decision'), + 'disabled' => true, + 'value' => $this->getPopulatedValue('val_' . $i) + ]); + } + + $this->registerElement($val); + $removeButton = null; + if ($recipientCount > 1) { + $removeButton = $this->createRemoveButton($i); + if ($removeButton->hasBeenPressed()) { + $removePosition = $i; + } + } + + $this->recipients[$i] = new EscalationRecipientListItem($i, $col, $val, $removeButton); + } + + if ($removePosition) { + $recipientCount -= 1; + $this->getElement('recipient-count')->setValue($recipientCount); + } + + $this->add(new EscalationRecipientList($this->recipients)); + + $this->addElement($addRecipientButton); + } + + /** + * Fetch recipient options + * + * @return array> + */ + protected function fetchOptions(): array + { + $options = []; + /** @var Contact $contact */ + foreach (Contact::on(Database::get()) as $contact) { + $options['Contacts']['contact_' . $contact->id] = $contact->full_name; + } + + /** @var Contactgroup $contactgroup */ + foreach (Contactgroup::on(Database::get()) as $contactgroup) { + $options['Contact Groups']['contactgroup_' . $contactgroup->id] = $contactgroup->name; + } + + /** @var Schedule $schedule */ + foreach (Schedule::on(Database::get()) as $schedule) { + $options['Schedules']['schedule_' . $schedule->id] = $schedule->name; + } + + return $options; + } + + /** + * Create remove button for the recipient in the given position + * + * @param int $pos + * + * @return SubmitButtonElement + */ + protected function createRemoveButton(int $pos): SubmitButtonElement + { + $removeButton = new SubmitButtonElement( + 'remove', + [ + 'class' => ['remove-button', 'control-button', 'spinner'], + 'label' => new Icon('minus'), + 'title' => $this->translate('Remove'), + 'formnovalidate' => true, + 'value' => (string) $pos + ] + ); + + $this->registerElement($removeButton); + + return $removeButton; + } + + public function hasValue(): bool + { + $this->ensureAssembled(); + + return parent::hasValue(); + } + + /** + * Get recipients of the escalation + * + * @return array> + */ + public function getRecipients(): array + { + /** @var int $count */ + $count = $this->getValue('recipient-count'); + $removePosition = $this->getValue('remove'); + if ($removePosition) { + $count += 1; + } + + $values = []; + for ($i = 1; $i <= $count; $i++) { + if ($i === (int) $removePosition) { + continue; + } + + $value = []; + $value['channel_id'] = $this->getValue('val_' . $i); + $value['id'] = $this->getValue('id_' . $i); + + /** @var ?string $columnName */ + $columnName = $this->getValue('column_' . $i); + + if ($columnName === null) { + $values[] = $value; + + continue; + } + + [$columnName, $id] = explode('_', $columnName, 2); + + $value[$columnName . '_id'] = $id; + + $values[] = $value; + } + + return $values; + } +} diff --git a/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php b/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php new file mode 100644 index 00000000..b723be2a --- /dev/null +++ b/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php @@ -0,0 +1,95 @@ + 'config-filter']; + + public function __construct(Url $searchEditorUrl, ?string $filter) + { + $this->searchEditorUrl = $searchEditorUrl; + $this->objectFilter = $filter; + + parent::__construct('config-filter'); + } + + protected function assemble(): void + { + if (! $this->getObjectFilter()) { + $addFilterButton = new SubmitButtonElement( + 'add-filter', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'formnovalidate' => true, + 'title' => $this->translate('Add filter') + ] + ); + $this->registerElement($addFilterButton); + + if ($addFilterButton->hasBeenPressed()) { + $this->removeAttribute('class', 'empty-filter'); + } else { + $this->addAttributes(['class' => 'empty-filter']); + $this->addHtml($addFilterButton); + + return; + } + } + + $editorOpener = new Link( + new Icon('cog'), + $this->searchEditorUrl, + Attributes::create([ + 'class' => ['search-editor-opener', 'control-button'], + 'title' => $this->translate('Adjust Filter'), + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true, + ]) + ); + + $searchBar = new TextElement( + 'searchbar', + [ + 'class' => ['filter-input', 'control-button'], + 'readonly' => true, + 'value' => $this->getObjectFilter() + ] + ); + + $filterElement = new HtmlElement( + 'div', + Attributes::create(['class' => ['search-controls', 'icinga-controls']]) + ); + + $filterElement->addHtml($searchBar, $editorOpener); + + $this->addHtml($filterElement); + } + + /** + * Get the event rule's object filter + * + * @return ?string + */ + public function getObjectFilter(): ?string + { + return $this->objectFilter; + } +} diff --git a/application/forms/EventRuleConfigForm.php b/application/forms/EventRuleConfigForm.php new file mode 100644 index 00000000..654de5c4 --- /dev/null +++ b/application/forms/EventRuleConfigForm.php @@ -0,0 +1,647 @@ + ['event-rule-config', 'icinga-form', 'icinga-controls'], + 'name' => 'event-rule-config-form', + 'id' => 'event-rule-config-form' + ]; + + /** @var array */ + protected $config; + + /** @var Url Search editor URL for the config filter fieldset */ + protected $searchEditorUrl; + + /** @var bool Whether the config has an escalation with no condition */ + protected $hasZeroConditionEscalation = false; + + /** + * Create a new EventRuleConfigForm + * + * @param array $config + * @param Url $searchEditorUrl + */ + public function __construct(array $config, Url $searchEditorUrl) + { + $this->config = $config; + $this->searchEditorUrl = $searchEditorUrl; + + $this->on(self::ON_SENT, function () { + $config = array_merge($this->config, $this->getValues()); + + if ($config !== $this->config) { + $this->emit(self::ON_CHANGE, [$this]); + } + }); + } + + public function hasBeenSubmitted() + { + $pressedButton = $this->getPressedSubmitElement(); + + if ($pressedButton) { + $buttonName = $pressedButton->getName(); + + if ($buttonName === 'delete') { + $this->emit(self::ON_DELETE, [$this]); + } elseif ($buttonName === 'discard_changes') { + $this->emit(self::ON_DISCARD, [$this]); + } elseif ($buttonName === 'save') { + return true; + } + } + + return false; + } + + /** + * Check whether the config has an escalation with no condition + * + * @return bool + */ + public function hasZeroConditionEscalation(): bool + { + return $this->hasZeroConditionEscalation; + } + + protected function assemble(): void + { + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + + // Replicate save button outside the form + $this->addElement( + 'submitButton', + 'save', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + // Replicate delete button outside the form + $this->addElement( + 'submitButton', + 'delete', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + // Replicate discard_changes button outside the form + $this->addElement( + 'submitButton', + 'discard_changes', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + $defaultEscalationPrefix = bin2hex('1'); + + $this->addElement('hidden', 'zero-condition-escalation'); + + if (! isset($this->config['rule_escalation'])) { + $this->getElement('zero-condition-escalation')->setValue($defaultEscalationPrefix); + } + + $configFilter = new EventRuleConfigFilter($this->searchEditorUrl, $this->config['object_filter']); + $this->registerElement($configFilter); + + $addEscalationButton = new SubmitButtonElement( + 'add-escalation', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'title' => $this->translate('Add Escalation'), + 'formnovalidate' => true + ] + ); + + $this->registerElement($addEscalationButton); + $prefixesElement = $this->createElement('hidden', 'prefixes-map', ['value' => $defaultEscalationPrefix]); + $this->addElement($prefixesElement); + $this->handleAdd(); + + $prefixesMapString = $prefixesElement->getValue(); + $prefixesMap = explode(',', $prefixesMapString); + $escalationCount = count($prefixesMap); + $zeroConditionEscalation = $this->getValue('zero-condition-escalation'); + $removePosition = null; + $removeEscalationButtons = []; + + if ($escalationCount > 1) { + foreach ($prefixesMap as $prefixMap) { + $removeEscalationButtons[$prefixMap] = $this->createRemoveButton($prefixMap); + } + + $removePosition = $this->getValue('remove-escalation'); + if ($removePosition && $escalationCount === 2) { + $removeEscalationButtons = []; + } + } + + $escalations = []; + $this->hasZeroConditionEscalation = $zeroConditionEscalation !== null; + + foreach ($prefixesMap as $key => $prefixMap) { + if ($removePosition === $prefixMap) { + if ($zeroConditionEscalation === $prefixMap) { + $zeroConditionEscalation = null; + $this->hasZeroConditionEscalation = false; + } + + unset($prefixesMap[$key]); + $this->getElement('prefixes-map')->setValue(implode(',', $prefixesMap)); + + continue; + } + + $escalationCondition = new EscalationCondition($prefixMap, $this); + $escalationRecipient = new EscalationRecipient($prefixMap); + $this->registerElement($escalationCondition); + $this->registerElement($escalationRecipient); + + $escalation = new Escalation( + $escalationCondition, + $escalationRecipient, + $removeEscalationButtons[$prefixMap] ?? null + ); + + if ($zeroConditionEscalation === $prefixMap && $escalation->addConditionHasBeenPressed()) { + $this->hasZeroConditionEscalation = false; + $zeroConditionEscalation = null; + } elseif ($escalation->lastConditionHasBeenRemoved()) { + $this->hasZeroConditionEscalation = true; + $zeroConditionEscalation = $prefixMap; + } + + $escalations[] = $escalation; + } + + $this->getElement('zero-condition-escalation')->setValue($zeroConditionEscalation); + + $this->addHtml( + (new HtmlElement('div', Attributes::create(['class' => 'filter-wrapper']))) + ->addHtml( + (new FlowLine())->getRightArrow(), + $configFilter, + (new FlowLine())->getHorizontalLine() + ) + ); + + $escalationWrapper = (new HtmlElement('div'))->addHtml(new Escalations($escalations), $addEscalationButton); + $this->addHtml($escalationWrapper); + } + + /** + * Handle addition of escalations + * + * @return void + */ + protected function handleAdd(): void + { + $pressedButton = $this->getPressedSubmitElement(); + + if ($pressedButton && $pressedButton->getName() === 'add-escalation') { + $this->clearPopulatedValue('prefixes-map'); + $prefixesMapString = $this->getValue('prefixes-map', ''); + $prefixesMap = explode(',', $prefixesMapString); + $escalationFakePos = bin2hex(random_bytes(4)); + $prefixesMap[] = $escalationFakePos; + $this->getElement('prefixes-map') + ->setValue(implode(',', $prefixesMap)); + + if ($this->getValue('zero-condition-escalation') === null) { + $this->getElement('zero-condition-escalation') + ->setValue($escalationFakePos); + } + } + } + + public function populate($values): self + { + if (! isset($values['rule_escalation'])) { + return parent::populate($values); + } + + $formValues = []; + $formValues['prefixes-map'] = $this->getPrefixesMap(count($values['rule_escalation'])); + + foreach ($values['rule_escalation'] as $position => $escalation) { + $conditions = explode('|', $escalation['condition'] ?? ''); + $conditionFormValues = []; + $conditionFormValues['condition-count'] = count($conditions); + $conditionFormValues['id'] = $escalation['id'] ?? bin2hex(random_bytes(4)); + + foreach ($conditions as $key => $condition) { + if ($condition === '' && ! isset($formValues['zero-condition-escalation'])) { + $formValues['zero-condition-escalation'] = bin2hex($position); + $conditionFormValues['condition-count'] = 0; + + continue; + } + + $count = $key + 1; + + /** @var Condition $filter */ + $filter = QueryString::parse($condition); + $conditionFormValues['column_' . $count] = $filter->getColumn() === 'placeholder' + ? null + : $filter->getColumn(); + + if ($conditionFormValues['column_' . $count]) { + $conditionFormValues['type_' . $count] = $conditionFormValues['column_' . $count]; + } + + $conditionFormValues['operator_' . $count] = QueryString::getRuleSymbol($filter); + $conditionFormValues['val_' . $count] = $filter->getValue(); + } + + $formValues['escalation-condition_' . bin2hex($position)] = $conditionFormValues; + $recipientFormValues = []; + if (isset($escalation['recipients'])) { + $recipientFormValues['recipient-count'] = count($escalation['recipients']); + foreach ($escalation['recipients'] as $key => $recipient) { + if (is_array($recipient)) { + $count = 0; + foreach ($recipient as $elementName => $elementValue) { + if ($elementValue === null) { + continue; + } + + $count = $key + 1; + $selectedOption = str_replace('id', $elementValue, $elementName, $replaced); + if ($replaced && $elementName !== 'channel_id') { + $recipientFormValues['column_' . $count] = $selectedOption; + } elseif ($elementName === 'channel_id') { + $recipientFormValues['val_' . $count] = $elementValue; + } + } + + if (isset($recipient['id'])) { + $recipientFormValues['id_' . $count] = (int) $recipient['id']; + } + } + } + } + + $formValues['escalation-recipient_' . bin2hex($position)] = $recipientFormValues; + } + + return parent::populate($formValues); + } + + /** + * Get the values for the current EventRuleConfigForm + * + * @return array values as name-value pairs + */ + public function getValues(): array + { + $values = []; + $escalations = []; + $prefixesString = $this->getValue('prefixes-map', ''); + + /** @var string[] $prefixesMap */ + $prefixesMap = explode(',', $prefixesString); + $i = 1; + foreach ($prefixesMap as $prefixMap) { + /** @var EscalationCondition $escalationCondition */ + $escalationCondition = $this->getElement('escalation-condition_' . $prefixMap); + /** @var EscalationRecipient $escalationRecipient */ + $escalationRecipient = $this->getElement('escalation-recipient_' . $prefixMap); + $escalations[$i]['condition'] = $escalationCondition->getCondition(); + $escalations[$i]['id'] = $escalationCondition->getValue('id'); + $escalations[$i]['recipients'] = $escalationRecipient->getRecipients(); + $i++; + } + + /** @var EventRuleConfigFilter $configFilter */ + $configFilter = $this->getElement('config-filter'); + $values['object_filter'] = $configFilter->getObjectFilter(); + $values['rule_escalation'] = $escalations; + + return $values; + } + + /** + * Create remove button for the given escalation position + * + * @param string $prefix + * + * @return SubmitButtonElement + */ + protected function createRemoveButton(string $prefix): SubmitButtonElement + { + /** @var array> $escalations */ + $escalations = $this->config['rule_escalation'] ?? []; + $pos = hex2bin($prefix); + $disableRemoveButton = false; + $escalationId = $escalations[$pos]['id'] ?? null; + + if ($escalationId && ctype_digit($escalationId)) { + $incidentCount = Incident::on(Database::get()) + ->with('rule_escalation') + ->filter(Filter::equal('rule_escalation.id', $escalationId)) + ->count(); + + $disableRemoveButton = $incidentCount > 0; + } + + $button = new SubmitButtonElement( + 'remove-escalation', + [ + 'class' => ['remove-escalation', 'remove-button', 'control-button', 'spinner'], + 'label' => new Icon('minus'), + 'formnovalidate' => true, + 'value' => $prefix, + 'disabled' => $disableRemoveButton, + 'title' => $disableRemoveButton + ? $this->translate('There are active incidents for this escalation and hence cannot be removed') + : $this->translate('Remove escalation') + ] + ); + + $this->registerElement($button); + + return $button; + } + + /** + * Insert to or update event rule in the database and return the id of the event rule + * + * @param int $id The id of the event rule + * @param array $config The new configuration + * + * @return int + */ + public function addOrUpdateRule(int $id, array $config): int + { + $db = Database::get(); + + $db->beginTransaction(); + + if ($id < 0) { + $db->insert('rule', [ + 'name' => $config['name'], + 'timeperiod_id' => $config['timeperiod_id'] ?? null, + 'object_filter' => $config['object_filter'] ?? null, + 'is_active' => $config['is_active'] ?? 'n' + ]); + + $id = $db->lastInsertId(); + } else { + $db->update('rule', [ + 'name' => $config['name'], + 'timeperiod_id' => $config['timeperiod_id'] ?? null, + 'object_filter' => $config['object_filter'] ?? null, + 'is_active' => $config['is_active'] ?? 'n' + ], ['id = ?' => $id]); + } + + $escalationsFromDb = RuleEscalation::on($db) + ->filter(Filter::equal('rule_id', $id)); + + $escalationsInCache = $config['rule_escalation']; + + $escalationsToUpdate = []; + $escalationsToRemove = []; + + /** @var RuleEscalation $escalationFromDB */ + foreach ($escalationsFromDb as $escalationFromDB) { + $escalationId = $escalationFromDB->id; + $escalationInCache = array_filter($escalationsInCache, function (array $element) use ($escalationId) { + /** @var string $idInCache */ + $idInCache = $element['id'] ?? null; + + return (int) $idInCache === $escalationId; + }); + + if ($escalationInCache) { + $position = array_key_first($escalationInCache); + // Escalations in DB to update + $escalationsToUpdate[$position] = $escalationInCache[$position]; + + unset($escalationsInCache[$position]); + } else { + // Escalation in DB to remove + $escalationsToRemove[] = $escalationId; + } + } + + // Escalations to add + $escalationsToAdd = $escalationsInCache; + + if (! empty($escalationsToRemove)) { + $db->delete('rule_escalation_recipient', ['rule_escalation_id IN (?)' => $escalationsToRemove]); + $db->delete('rule_escalation', ['id IN (?)' => $escalationsToRemove]); + } + + if (! empty($escalationsToAdd)) { + $this->insertOrUpdateEscalations($id, $escalationsToAdd, true); + } + + if (! empty($escalationsToUpdate)) { + $this->insertOrUpdateEscalations($id, $escalationsToUpdate); + } + + $db->commitTransaction(); + + return (int) $id; + } + + /** + * Insert to or update escalations in Db + * + * @param int $ruleId + * @param array> $escalations + * @param bool $insert + * + * @return void + */ + private function insertOrUpdateEscalations(int $ruleId, array $escalations, bool $insert = false): void + { + $db = Database::get(); + foreach ($escalations as $position => $escalationConfig) { + $recipientsFromConfig = $escalationConfig['recipients'] ?? []; + if ($insert) { + $db->insert('rule_escalation', [ + 'rule_id' => $ruleId, + 'position' => $position, + 'condition' => $escalationConfig['condition'] ?? null, + 'name' => $escalationConfig['name'] ?? null, + 'fallback_for' => $escalationConfig['fallback_for'] ?? null + ]); + + $escalationId = $db->lastInsertId(); + } else { + /** @var string $escalationId */ + $escalationId = $escalationConfig['id']; + $db->update('rule_escalation', [ + 'position' => $position, + 'condition' => $escalationConfig['condition'] ?? null, + 'name' => $escalationConfig['name'] ?? null, + 'fallback_for' => $escalationConfig['fallback_for'] ?? null + ], ['id = ?' => $escalationId, 'rule_id = ?' => $ruleId]); + + $recipientsToRemove = []; + $recipients = RuleEscalationRecipient::on($db) + ->columns('id') + ->filter(Filter::equal('rule_escalation_id', $escalationId)); + + /** @var RuleEscalationRecipient $recipient */ + foreach ($recipients as $recipient) { + $recipientId = $recipient->id; + $recipientInCache = array_filter( + $recipientsFromConfig, + function (array $element) use ($recipientId) { + /** @var string $idFromCache */ + $idFromCache = $element['id']; + return (int) $idFromCache === $recipientId; + } + ); + + if (empty($recipientInCache)) { + // Recipients to remove from Db not in cache + $recipientsToRemove[] = $recipientId; + } + } + + if (! empty($recipientsToRemove)) { + $db->delete('rule_escalation_recipient', ['id IN (?)' => $recipientsToRemove]); + } + } + + foreach ($recipientsFromConfig as $recipientConfig) { + $data = [ + 'rule_escalation_id' => $escalationId, + 'channel_id' => $recipientConfig['channel_id'] + ]; + + switch (true) { + case isset($recipientConfig['contact_id']): + $data['contact_id'] = $recipientConfig['contact_id']; + $data['contactgroup_id'] = null; + $data['schedule_id'] = null; + + break; + case isset($recipientConfig['contactgroup_id']): + $data['contact_id'] = null; + $data['contactgroup_id'] = $recipientConfig['contactgroup_id']; + $data['schedule_id'] = null; + + break; + case isset($recipientConfig['schedule_id']): + $data['contact_id'] = null; + $data['contactgroup_id'] = null; + $data['schedule_id'] = $recipientConfig['schedule_id']; + + break; + } + + if (! isset($recipientConfig['id'])) { + $db->insert('rule_escalation_recipient', $data); + } else { + $db->update('rule_escalation_recipient', $data, ['id = ?' => $recipientConfig['id']]); + } + } + } + } + + public function isValidEvent($event) + { + if (in_array($event, [self::ON_CHANGE, self::ON_DELETE, self::ON_DISCARD])) { + return true; + } + + return parent::isValidEvent($event); + } + + /** + * Remove the given event rule + * + * @param int $id + * + * @return void + */ + public function removeRule(int $id): void + { + $db = Database::get(); + $db->beginTransaction(); + $escalations = RuleEscalation::on($db) + ->columns('id') + ->filter(Filter::equal('rule_id', $id)); + + $escalationsToRemove = []; + /** @var RuleEscalation $escalation */ + foreach ($escalations as $escalation) { + $escalationsToRemove[] = $escalation->id; + } + + if (! empty($escalationsToRemove)) { + $db->delete('rule_escalation_recipient', ['rule_escalation_id IN (?)' => $escalationsToRemove]); + } + + $db->delete('rule_escalation', ['rule_id = ?' => $id]); + $db->delete('rule', ['id = ?' => $id]); + + $db->commitTransaction(); + } + + /** + * Get the prefix map + * + * @param int $escalationCount + * + * @return string + */ + protected function getPrefixesMap(int $escalationCount): string + { + $prefixesMap = []; + for ($i = 1; $i <= $escalationCount; $i++) { + $prefixesMap[] = bin2hex((string) $i); + } + + return implode(',', $prefixesMap); + } +} diff --git a/application/forms/EventRuleForm.php b/application/forms/EventRuleForm.php index 9ce497ef..13af46dc 100644 --- a/application/forms/EventRuleForm.php +++ b/application/forms/EventRuleForm.php @@ -14,9 +14,9 @@ class EventRuleForm extends CompatForm use CsrfCounterMeasure; use Translation; - protected function assemble() + protected function assemble(): void { - $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); + $this->addHtml($this->createCsrfCounterMeasure(Session::getSession()->getId())); $this->addElement( 'text', diff --git a/application/forms/RemoveEscalationForm.php b/application/forms/RemoveEscalationForm.php deleted file mode 100644 index 4bc9cb79..00000000 --- a/application/forms/RemoveEscalationForm.php +++ /dev/null @@ -1,70 +0,0 @@ - ['remove-escalation-form', 'icinga-form', 'icinga-controls'], - ]; - - /** @var bool */ - private $disableRemoveButtton; - - protected function assemble() - { - $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); - $this->add($this->createUidElement()); - - $this->addElement( - 'submitButton', - 'remove', - [ - 'class' => ['remove-button', 'control-button', 'spinner'], - 'label' => new Icon('minus') - ] - ); - - $this->getElement('remove') - ->getAttributes() - ->registerAttributeCallback('disabled', function () { - return $this->disableRemoveButtton; - }) - ->registerAttributeCallback('title', function () { - if ($this->disableRemoveButtton) { - return $this->translate( - 'There exist active incidents for this escalation and hence cannot be removed' - ); - } - - return $this->translate('Remove escalation'); - }); - } - - /** - * Method to set disabled state of remove button - * - * @param bool $state - * - * @return $this - */ - public function setRemoveButtonDisabled(bool $state = false) - { - $this->disableRemoveButtton = $state; - - return $this; - } -} diff --git a/application/forms/SaveEventRuleForm.php b/application/forms/SaveEventRuleForm.php deleted file mode 100644 index f7bd7aa0..00000000 --- a/application/forms/SaveEventRuleForm.php +++ /dev/null @@ -1,481 +0,0 @@ - ['icinga-controls', 'save-event-rule'], - 'name' => 'save-event-rule' - ]; - - /** @var bool Whether to disable the submit button */ - protected $disableSubmitButton = false; - - /** @var string The label to use on the submit button */ - protected $submitLabel; - - /** @var bool Whether to show a button to delete the rule */ - protected $showRemoveButton = false; - - /** @var bool Whether to show a button to dismiss cached changes */ - protected $showDismissChangesButton = false; - - /** @var bool Whether to disable the remove button */ - protected $disableRemoveButton = false; - - /** - * Create a new SaveEventRuleForm - */ - public function __construct() - { - $this->on(self::ON_SENT, function () { - if ($this->hasBeenRemoved()) { - $this->emit(self::ON_REMOVE, [$this]); - } - }); - } - - public function hasBeenSubmitted(): bool - { - return $this->hasBeenSent() && $this->getPressedSubmitElement() !== null; - } - - /** - * Set whether to enable or disable the submit button - * - * @param bool $state - * - * @return $this - */ - public function setSubmitButtonDisabled(bool $state = true): self - { - $this->disableSubmitButton = $state; - - return $this; - } - - /** - * Set whether to enable or disable the remove button - * - * @param bool $state - * - * @return $this - */ - public function setRemoveButtonDisabled(bool $state = true): self - { - $this->disableRemoveButton = $state; - - return $this; - } - - /** - * Set the submit label - * - * @param string $label - * - * @return $this - */ - public function setSubmitLabel(string $label): self - { - $this->submitLabel = $label; - - return $this; - } - - /** - * Get the submit label - * - * @return string - */ - public function getSubmitLabel(): string - { - return $this->submitLabel ?? t('Add Event Rule'); - } - - /** - * Set whether to show a button to delete the rule - * - * @param bool $state - * - * @return $this - */ - public function setShowRemoveButton(bool $state = true): self - { - $this->showRemoveButton = $state; - - return $this; - } - - /** - * Set whether to show a button to dismiss cached changes - * - * @param bool $state - * - * @return $this - */ - public function setShowDismissChangesButton(bool $state = true): self - { - $this->showDismissChangesButton = $state; - - return $this; - } - - /** - * Get whether the user pushed the remove button - * - * @return bool - */ - private function hasBeenRemoved(): bool - { - $btn = $this->getPressedSubmitElement(); - $csrf = $this->getElement('CSRFToken'); - - return $csrf !== null && $csrf->isValid() && $btn !== null && $btn->getName() === 'remove'; - } - - public function isValidEvent($event) - { - if ($event === self::ON_REMOVE) { - return true; - } - - return parent::isValidEvent($event); - } - - protected function assemble() - { - $this->addElement($this->createUidElement()); - $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); - - $this->addElement('submit', 'submit', [ - 'label' => $this->getSubmitLabel(), - 'class' => 'btn-primary' - ]); - - $this->getElement('submit') - ->getAttributes() - ->registerAttributeCallback('disabled', function () { - return $this->disableSubmitButton; - }); - - $additionalButtons = []; - if ($this->showRemoveButton) { - $removeBtn = $this->createElement('submit', 'remove', [ - 'label' => $this->translate('Delete Event Rule'), - 'class' => 'btn-remove', - 'formnovalidate' => true - ]); - $this->registerElement($removeBtn); - - $this->getElement('remove') - ->getAttributes() - ->registerAttributeCallback('disabled', function () { - return $this->disableRemoveButton; - }) - ->registerAttributeCallback('title', function () { - if ($this->disableRemoveButton) { - return $this->translate( - 'There exist active incidents for this event rule and hence cannot be deleted' - ); - } - }); - - $additionalButtons[] = $removeBtn; - } - - if ($this->showDismissChangesButton) { - $clearCacheBtn = $this->createElement('submit', 'discard_changes', [ - 'label' => $this->translate('Discard Changes'), - 'class' => 'btn-discard-changes', - 'formnovalidate' => true - ]); - $this->registerElement($clearCacheBtn); - $additionalButtons[] = $clearCacheBtn; - } - - $this->getElement('submit')->prependWrapper((new HtmlDocument())->setHtmlContent(...$additionalButtons)); - } - - /** - * Add a new event rule with the given configuration - * - * @param array $config - * - * @return int The id of the new event rule - */ - public function addRule(array $config): int - { - if (! isset($config['name'])) { - throw new Exception('Name of the event rule is not set'); - } - - $db = Database::get(); - - $db->beginTransaction(); - - $db->insert('rule', [ - 'name' => $config['name'], - 'timeperiod_id' => $config['timeperiod_id'] ?? null, - 'object_filter' => $config['object_filter'] ?? null, - 'is_active' => $config['is_active'] ?? 'n' - ]); - $ruleId = $db->lastInsertId(); - - foreach ($config['rule_escalation'] ?? [] as $position => $escalationConfig) { - $db->insert('rule_escalation', [ - 'rule_id' => $ruleId, - 'position' => $position, - 'condition' => $escalationConfig['condition'] ?? null, - 'name' => $escalationConfig['name'] ?? null, - 'fallback_for' => $escalationConfig['fallback_for'] ?? null - ]); - $escalationId = $db->lastInsertId(); - - foreach ($escalationConfig['recipient'] ?? [] as $recipientConfig) { - $data = [ - 'rule_escalation_id' => $escalationId, - 'channel_id' => $recipientConfig['channel_id'] - ]; - - switch (true) { - case isset($recipientConfig['contact_id']): - $data['contact_id'] = $recipientConfig['contact_id']; - break; - case isset($recipientConfig['contactgroup_id']): - $data['contactgroup_id'] = $recipientConfig['contactgroup_id']; - break; - case isset($recipientConfig['schedule_id']): - $data['schedule_id'] = $recipientConfig['schedule_id']; - break; - } - - $db->insert('rule_escalation_recipient', $data); - } - } - - $db->commitTransaction(); - - return $ruleId; - } - - /** - * Insert to or update Escalations and its recipients in Db - * - * @param $ruleId - * @param array $escalations - * @param Connection $db - * @param bool $insert - * - * @return void - */ - private function insertOrUpdateEscalations($ruleId, array $escalations, Connection $db, bool $insert = false): void - { - foreach ($escalations as $position => $escalationConfig) { - if ($insert) { - $db->insert('rule_escalation', [ - 'rule_id' => $ruleId, - 'position' => $position, - 'condition' => $escalationConfig['condition'] ?? null, - 'name' => $escalationConfig['name'] ?? null, - 'fallback_for' => $escalationConfig['fallback_for'] ?? null - ]); - - $escalationId = $db->lastInsertId(); - } else { - $escalationId = $escalationConfig['id']; - - $db->update('rule_escalation', [ - 'position' => $position, - 'condition' => $escalationConfig['condition'] ?? null, - 'name' => $escalationConfig['name'] ?? null, - 'fallback_for' => $escalationConfig['fallback_for'] ?? null - ], ['id = ?' => $escalationId, 'rule_id = ?' => $ruleId]); - $recipientsToRemove = []; - - $recipients = RuleEscalationRecipient::on($db) - ->columns('id') - ->filter(Filter::equal('rule_escalation_id', $escalationId)); - - foreach ($recipients as $recipient) { - $recipientId = $recipient->id; - $recipientInCache = array_filter( - $escalationConfig['recipient'], - function (array $element) use ($recipientId) { - return (int) $element['id'] === $recipientId; - } - ); - - if (empty($recipientInCache)) { - // Recipients to remove from Db not in cache - $recipientsToRemove[] = $recipientId; - } - } - - if (! empty($recipientsToRemove)) { - $db->delete('rule_escalation_recipient', ['id IN (?)' => $recipientsToRemove]); - } - } - - foreach ($escalationConfig['recipient'] ?? [] as $recipientConfig) { - $data = [ - 'rule_escalation_id' => $escalationId, - 'channel_id' => $recipientConfig['channel_id'] - ]; - - switch (true) { - case isset($recipientConfig['contact_id']): - $data['contact_id'] = $recipientConfig['contact_id']; - $data['contactgroup_id'] = null; - $data['schedule_id'] = null; - break; - case isset($recipientConfig['contactgroup_id']): - $data['contact_id'] = null; - $data['contactgroup_id'] = $recipientConfig['contactgroup_id']; - $data['schedule_id'] = null; - break; - case isset($recipientConfig['schedule_id']): - $data['contact_id'] = null; - $data['contactgroup_id'] = null; - $data['schedule_id'] = $recipientConfig['schedule_id']; - break; - } - - if (! isset($recipientConfig['id'])) { - $db->insert('rule_escalation_recipient', $data); - } else { - $db->update('rule_escalation_recipient', $data, ['id = ?' => $recipientConfig['id']]); - } - } - } - } - - /** - * Edit an existing event rule - * - * @param int $id The id of the event rule - * @param array $config The new configuration - * - * @return void - */ - public function editRule(int $id, array $config): void - { - $db = Database::get(); - - $db->beginTransaction(); - - $db->update('rule', [ - 'name' => $config['name'], - 'timeperiod_id' => $config['timeperiod_id'] ?? null, - 'object_filter' => $config['object_filter'] ?? null, - 'is_active' => $config['is_active'] ?? 'n' - ], ['id = ?' => $id]); - - $escalationsFromDb = RuleEscalation::on($db) - ->filter(Filter::equal('rule_id', $id)); - - $escalationsInCache = $config['rule_escalation']; - - $escalationsToUpdate = []; - $escalationsToRemove = []; - - foreach ($escalationsFromDb as $escalationInDB) { - $escalationId = $escalationInDB->id; - $escalationInCache = array_filter($escalationsInCache, function (array $element) use ($escalationId) { - return (int) $element['id'] === $escalationId; - }); - - if ($escalationInCache) { - $position = array_key_first($escalationInCache); - // Escalations in DB to update - $escalationsToUpdate[$position] = $escalationInCache[$position]; - unset($escalationsInCache[$position]); - } else { - // Escalation in DB to remove - $escalationsToRemove[] = $escalationId; - } - } - - // Escalations to add - $escalationsToAdd = $escalationsInCache; - - if (! empty($escalationsToRemove)) { - $db->delete('rule_escalation_recipient', ['rule_escalation_id IN (?)' => $escalationsToRemove]); - $db->delete('rule_escalation', ['id IN (?)' => $escalationsToRemove]); - } - - if (! empty($escalationsToAdd)) { - $this->insertOrUpdateEscalations($id, $escalationsToAdd, $db, true); - } - - if (! empty($escalationsToUpdate)) { - $this->insertOrUpdateEscalations($id, $escalationsToUpdate, $db); - } - - $db->commitTransaction(); - } - - /** - * Remove the given event rule - * - * @param int $id - * - * @return void - */ - public function removeRule(int $id): void - { - $db = Database::get(); - - $db->beginTransaction(); - - $escalations = RuleEscalation::on($db) - ->columns('id') - ->filter(Filter::equal('rule_id', $id)); - - $escalationsToRemove = []; - foreach ($escalations as $escalation) { - $escalationsToRemove[] = $escalation->id; - } - - if (! empty($escalationsToRemove)) { - $db->delete('rule_escalation_recipient', ['rule_escalation_id IN (?)' => $escalationsToRemove]); - } - - $db->delete('rule_escalation', ['rule_id = ?' => $id]); - $db->delete('rule', ['id = ?' => $id]); - - $db->commitTransaction(); - } - - protected function onError() - { - foreach ($this->getMessages() as $message) { - if ($message instanceof Exception) { - Notification::error($this->translate($message->getMessage())); - } - } - } -} diff --git a/library/Notifications/Common/Links.php b/library/Notifications/Common/Links.php index fbdd03e5..b2f040d3 100644 --- a/library/Notifications/Common/Links.php +++ b/library/Notifications/Common/Links.php @@ -110,4 +110,9 @@ public static function moveRotation(): Url { return Url::fromPath('notifications/schedule/move-rotation'); } + + public static function ruleFilterSuggestionUrl(int $id): Url + { + return Url::fromPath("notifications/event-rule/complete", ['id' => $id]); + } } diff --git a/library/Notifications/Model/Contact.php b/library/Notifications/Model/Contact.php index 539e1728..4a200e96 100644 --- a/library/Notifications/Model/Contact.php +++ b/library/Notifications/Model/Contact.php @@ -7,10 +7,23 @@ use Icinga\Module\Notifications\Model\Behavior\HasAddress; use ipl\Orm\Behaviors; use ipl\Orm\Model; +use ipl\Orm\Query; use ipl\Orm\Relations; /** * @property int $id + * @property string $full_name + * @property ?string $username + * @property int $default_channel_id + * + * @property Query|Channel $channel + * @property Query|ContactAddress $contact_address + * @property Query|Incident $incident + * @property Query|IncidentContact $incident_contact + * @property Query|IncidentHistory $incident_history + * @property Query|RuleEscalationRecipient $rule_escalation_recipient + * @property Query|RotationMember $rotation_member + * @property Query|Contactgroup $contactgroup */ class Contact extends Model { @@ -33,7 +46,7 @@ public function getColumns(): array ]; } - public function getColumnDefinitions() + public function getColumnDefinitions(): array { return [ 'full_name' => t('Full Name'), @@ -41,22 +54,22 @@ public function getColumnDefinitions() ]; } - public function getSearchColumns() + public function getSearchColumns(): array { return ['full_name']; } - public function createBehaviors(Behaviors $behaviors) + public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new HasAddress()); } - public function getDefaultSort() + public function getDefaultSort(): array { return ['full_name']; } - public function createRelations(Relations $relations) + public function createRelations(Relations $relations): void { $relations->belongsTo('channel', Channel::class) ->setCandidateKey('default_channel_id'); diff --git a/library/Notifications/Model/RuleEscalation.php b/library/Notifications/Model/RuleEscalation.php index 78064eb6..da3da420 100644 --- a/library/Notifications/Model/RuleEscalation.php +++ b/library/Notifications/Model/RuleEscalation.php @@ -6,7 +6,22 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +use ipl\Orm\Query; +/** + * @property int $id + * @property int $rule_id + * @property int $position + * @property ?string $condition + * @property ?string $name + * @property ?int $fallback_for + * + * @property Query|Contact $contact + * @property Query|Incident $incident + * @property Query|IncidentHistory $incident_history + * @property Query|Rule $rule + * @property Query|RuleEscalationRecipient $rule_escalation_recipient + */ class RuleEscalation extends Model { public function getTableName() diff --git a/library/Notifications/Model/RuleEscalationRecipient.php b/library/Notifications/Model/RuleEscalationRecipient.php index a1f4bdcb..25f06ed1 100644 --- a/library/Notifications/Model/RuleEscalationRecipient.php +++ b/library/Notifications/Model/RuleEscalationRecipient.php @@ -6,7 +6,22 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +use ipl\Orm\Query; +/** + * @property int $id + * @property int $rule_escalation_id + * @property ?int $contact_id + * @property ?int $contactgroup_id + * @property ?int $schedule_id + * @property ?int $channel_id + * + * @property Query|Channel $channel + * @property Query|Contact $contact + * @property Query|Contactgroup $contactgroup + * @property Query|RuleEscalation $rule_escalation + * @property Query|Schedule $schedule + */ class RuleEscalationRecipient extends Model { public function getTableName() @@ -64,15 +79,21 @@ public function getRecipient() { $recipientModel = null; if ($this->contact_id) { - $recipientModel = $this->contact->first(); + /** @var Query $contact */ + $contact = $this->contact; + $recipientModel = $contact->first(); } if ($this->contactgroup_id) { - $recipientModel = $this->contactgroup->first(); + /** @var Query $contactgroup */ + $contactgroup = $this->contactgroup; + $recipientModel = $contactgroup->first(); } if ($this->schedule_id) { - $recipientModel = $this->schedule->first(); + /** @var Query $schedule */ + $schedule = $this->schedule; + $recipientModel = $schedule->first(); } return $recipientModel; diff --git a/library/Notifications/Widget/Escalations.php b/library/Notifications/Widget/Escalations.php deleted file mode 100644 index de216093..00000000 --- a/library/Notifications/Widget/Escalations.php +++ /dev/null @@ -1,64 +0,0 @@ - 'escalations']; - - protected $tag = 'div'; - - protected $config; - - private $escalations = []; - - protected function assemble() - { - $this->add($this->escalations); - } - - public function addEscalation(int $position, array $escalation, ?RemoveEscalationForm $removeEscalationForm = null) - { - $flowLine = (new FlowLine())->getRightArrow(); - - if ( - in_array( - 'count-zero-escalation-condition-form', - $escalation[0]->getAttributes()->get('class')->getValue() - ) - ) { - $flowLine->addAttributes(['class' => 'right-arrow-long']); - } - - if ($removeEscalationForm) { - $this->escalations[$position] = Html::tag( - 'div', - ['class' => 'escalation'], - [ - $removeEscalationForm, - $flowLine, - $escalation[0], - $flowLine, - $escalation[1], - ] - ); - } else { - $this->escalations[$position] = Html::tag( - 'div', - ['class' => 'escalation'], - [ - $flowLine->addAttributes(['class' => 'right-arrow-one-escalation']), - $escalation[0], - $flowLine, - $escalation[1] - ] - ); - } - } -} diff --git a/library/Notifications/Widget/EventRuleConfig.php b/library/Notifications/Widget/EventRuleConfig.php deleted file mode 100644 index 1ae0a4f9..00000000 --- a/library/Notifications/Widget/EventRuleConfig.php +++ /dev/null @@ -1,417 +0,0 @@ - 'event-rule-detail' - ]; - - public const ON_CHANGE = 'on_change'; - - protected $tag = 'div'; - - /** @var Form[] */ - private $forms; - - /** @var array The config */ - protected $config; - - /** @var Url The url to open the SearchEditor at */ - protected $searchEditorUrl; - - /** @var array> */ - private $escalationForms = []; - - /** @var array */ - private $removeEscalationForms; - - /** @var int */ - private $numEscalations; - - public function __construct(Url $searchEditorUrl, $config = []) - { - $this->searchEditorUrl = $searchEditorUrl; - $this->setConfig($config); - - $this->createForms(); - } - - protected function createForms(): void - { - $config = $this->getConfig(); - $addFilter = (new AddFilterForm()) - ->on(Form::ON_SENT, function () { - $this->config['showSearchbar'] = true; - - $this->emit(self::ON_CHANGE, [$this]); - }); - - $escalations = $config['rule_escalation'] ?? [1 => ['id' => $this->generateFakeEscalationId()]]; - - if (! isset($this->config['rule_escalation'])) { - $this->config['rule_escalation'] = $escalations; - } - - $addEscalation = (new AddEscalationForm()) - ->on(AddEscalationForm::ON_SENT, function () use ($escalations) { - $newPosition = (int) array_key_last($escalations) + 1; - $this->config['rule_escalation'][$newPosition] = ['id' => $this->generateFakeEscalationId()]; - if ($this->config['conditionPlusButtonPosition'] === null) { - $this->config['conditionPlusButtonPosition'] = $newPosition; - } - - $this->removeEscalationForms[$newPosition] = $this->createRemoveEscalationForm($newPosition); - - if ($newPosition === 2) { - $this->removeEscalationForms[1] = $this->createRemoveEscalationForm(1); - $this->forms[] = $this->removeEscalationForms[1]; - } - - $this->escalationForms[$newPosition] = [ - $this->createConditionForm($newPosition), - $this->createRecipientForm($newPosition) - ]; - - $this->emit(self::ON_CHANGE, [$this]); - }); - - $this->forms = [ - $addFilter, - $addEscalation - ]; - - foreach ($escalations as $position => $escalation) { - /** @var int $position */ - $values = explode('|', $escalation['condition'] ?? ''); - $escalationCondition = $this->createConditionForm($position, $values); - - $values = $escalation['recipient'] ?? []; - $escalationRecipient = $this->createRecipientForm($position, $values); - - $this->escalationForms[$position] = [ - $escalationCondition, - $escalationRecipient - ]; - - $this->forms[] = $escalationCondition; - $this->forms[] = $escalationRecipient; - - if (count($escalations) > 1) { - $removeEscalation = $this->createRemoveEscalationForm($position); - - $this->forms[] = $removeEscalation; - $this->removeEscalationForms[$position] = $removeEscalation; - } - } - } - - /** - * Create and return the SearchEditor - * - * @return SearchEditor - * - * @throws ProgrammingError - */ - public static function createSearchEditor(): SearchEditor - { - $editor = new SearchEditor(); - - $editor->setAction(Url::fromRequest()->getAbsoluteUrl()); - - $editor->setSuggestionUrl(Url::fromPath( - "notifications/event-rule/complete", - ['_disableLayout' => true, 'showCompact' => true, 'id' => Url::fromRequest()->getParams()->get('id')] - )); - - return $editor; - } - - public static function createFilterString($filters): ?string - { - foreach ($filters as $filter) { - if ($filter instanceof Filter\Chain) { - self::createFilterString($filter); - } elseif (empty($filter->getValue())) { - $filter->setValue(true); - } - } - - if ($filters instanceof Filter\Condition && empty($filters->getValue())) { - $filters->setValue(true); - } - - $filterStr = QueryString::render($filters); - - return ! empty($filterStr) ? $filterStr : null; - } - - public function getForms(): array - { - return $this->forms; - } - - protected function assemble() - { - [$addFilter, $addEscalation] = $this->forms; - - $addFilterButtonOrSearchBar = $addFilter; - $horizontalLine = (new FlowLine())->getHorizontalLine(); - if (! empty($this->config['showSearchbar'])) { - $editorOpener = new Link( - new Icon('cog'), - $this->searchEditorUrl, - Attributes::create([ - 'class' => 'search-editor-opener control-button', - 'title' => t('Adjust Filter'), - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true, - ]) - ); - - $searchBar = new TextElement( - 'searchbar', - [ - 'class' => 'filter-input control-button', - 'readonly' => true, - 'value' => isset($this->config['object_filter']) - ? rawurldecode($this->config['object_filter']) - : null - ] - ); - - $addFilterButtonOrSearchBar = Html::tag('div', ['class' => 'search-controls icinga-controls']); - $addFilterButtonOrSearchBar->add([$searchBar, $editorOpener]); - } else { - $horizontalLine->getAttributes() - ->add(['class' => 'horizontal-line-long']); - } - - $this->add([ - (new FlowLine())->getRightArrow(), - $addFilterButtonOrSearchBar, - $horizontalLine - ]); - - $escalations = new Escalations(); - - foreach ($this->escalationForms as $position => $escalation) { - if (isset($this->removeEscalationForms[$position])) { - $escalations->addEscalation($position, $escalation, $this->removeEscalationForms[$position]); - } else { - $escalations->addEscalation($position, $escalation); - } - } - - $escalationswithAdd = Html::tag('div', ['class' => 'escalations-with-add-form']); - - $escalationswithAdd->add([ - $escalations, - $addEscalation - ]); - - $this->add($escalationswithAdd); - } - - public function getConfig(): ?array - { - return $this->config; - } - - public function setConfig($config): self - { - $this->config = $config; - - return $this; - } - - public function isValid(): bool - { - foreach ($this->escalationForms as $escalation) { - [$conditionForm, $recipientForm] = $escalation; - - if (! $conditionForm->isValid() || ! $recipientForm->isValid()) { - return false; - } - } - - return true; - } - - private function createConditionForm(int $position, array $values = []): EscalationConditionForm - { - $cnt = empty(array_filter($values)) ? null : count($values); - - if (! array_key_exists('conditionPlusButtonPosition', $this->config)) { - //the default position of add condition button - $pos = null; - foreach ($this->config['rule_escalation'] as $p => $v) { - if (empty($v['condition'])) { - $pos = $p; - break; - } - } - - $this->config['conditionPlusButtonPosition'] = $pos; - } - - if ($cnt === null && $this->config['conditionPlusButtonPosition'] !== $position) { - $cnt = 1; - } - - $form = (new EscalationConditionForm($cnt)) - ->addAttributes(['name' => 'escalation-condition-form-' . $position]) - ->deleteRemoveButton($this->config['conditionPlusButtonPosition'] !== null) - ->on(Form::ON_SENT, function ($form) use ($position) { - $values = $form->getValues(); - if ( - $form->isAddButtonPressed() - && $this->config['conditionPlusButtonPosition'] === $position - && empty($this->config['rule_escalation'][$position]['condition']) - ) { - $this->config['conditionPlusButtonPosition'] = null; - } - if (empty($values)) { - $this->config['conditionPlusButtonPosition'] = $position; - } - - $this->config['rule_escalation'][$position]['condition'] = $values; - - $this->emit(self::ON_CHANGE, [$this]); - }); - - if ($cnt !== null) { - $form->populate($values); - } else { - $form->addAttributes(['class' => 'count-zero-escalation-condition-form']); - } - - return $form; - } - - private function createRecipientForm(int $position, array $values = []): EscalationRecipientForm - { - $cnt = empty(array_filter($values)) ? null : count($values); - $form = (new EscalationRecipientForm($cnt)) - ->addAttributes(['name' => 'escalation-recipient-form-' . $position]) - ->on(Form::ON_SENT, function ($form) use ($position) { - $this->config['rule_escalation'][$position]['recipient'] = $form->getValues(); - - $this->emit(self::ON_CHANGE, [$this]); - }); - - if ($cnt !== null) { - $form->populate($values); - } - - return $form; - } - - private function createRemoveEscalationForm(int $position): RemoveEscalationForm - { - $escalationId = $this->config['rule_escalation'][$position]['id']; - - $incident = Incident::on(Database::get()) - ->with('rule_escalation'); - - $disableRemoveButton = false; - if (is_int($escalationId)) { - $incident->filter(Filter::equal('rule_escalation.id', $escalationId)); - if ($incident->count() > 0) { - $disableRemoveButton = true; - } - } - - - $form = (new RemoveEscalationForm()) - ->addAttributes(['name' => 'remove-escalation-form-' . $escalationId]) - ->setRemoveButtonDisabled($disableRemoveButton) - ->on(Form::ON_SENT, function ($form) use ($position) { - unset($this->config['rule_escalation'][$position]); - unset($this->escalationForms[$position]); - unset($this->removeEscalationForms[$position]); - - if ($this->config['conditionPlusButtonPosition'] === $position) { - $this->config['conditionPlusButtonPosition'] = null; - } elseif ($this->config['conditionPlusButtonPosition'] > $position) { - $this->config['conditionPlusButtonPosition'] -= 1; - } - - if (! empty($this->config['rule_escalation'])) { - $this->config['rule_escalation'] = array_combine( - range( - 1, - count($this->config['rule_escalation']) - ), - array_values($this->config['rule_escalation']) - ); - } - - if (! empty($this->removeEscalationForms)) { - /** @var array $removeEscalationForms */ - $removeEscalationForms = array_combine( - range( - 1, - count($this->removeEscalationForms) - ), - array_values($this->removeEscalationForms) - ); - $this->removeEscalationForms = $removeEscalationForms; - } - - if (! empty($this->escalationForms)) { - /** @var array> $escalationForms */ - $escalationForms = array_combine( - range( - 1, - count($this->escalationForms) - ), - array_values($this->escalationForms) - ); - $this->escalationForms = $escalationForms; - } - - $numEscalation = count($this->escalationForms); - if ($numEscalation === 1) { - unset($this->removeEscalationForms[1]); - } - - $this->emit(self::ON_CHANGE, [$this]); - }); - - return $form; - } - - private function generateFakeEscalationId(): string - { - return bin2hex(random_bytes(4)); - } -} diff --git a/library/Notifications/Widget/ItemList/Escalation.php b/library/Notifications/Widget/ItemList/Escalation.php new file mode 100644 index 00000000..555f44ec --- /dev/null +++ b/library/Notifications/Widget/ItemList/Escalation.php @@ -0,0 +1,97 @@ + 'escalation']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button of the escalation */ + protected $removeButton; + + /** @var EscalationCondition Escalation condition fieldset */ + protected $condition; + + /** @var EscalationRecipient Escalation recipient fieldset */ + protected $recipient; + + /** @var bool Whether the widget has a remove button */ + protected $hasNoRemoveButton = false; + + /** + * Create the escalation list item + * + * @param EscalationCondition $condition + * @param EscalationRecipient $recipient + * @param ?SubmitButtonElement $removeButton + */ + public function __construct( + EscalationCondition $condition, + EscalationRecipient $recipient, + ?SubmitButtonElement $removeButton + ) { + $this->condition = $condition; + $this->recipient = $recipient; + $this->removeButton = $removeButton; + } + + /** + * Check if the add button of the condition fieldset has been pressed + * + * @return bool + */ + public function addConditionHasBeenPressed(): bool + { + return $this->condition->getPopulatedValue('add-condition') === 'y'; + } + + /** + * Check if the last condition of the escalation has been removed + * + * @return bool + */ + public function lastConditionHasBeenRemoved(): bool + { + return $this->condition->getPopulatedValue('condition-count') === '1' + && $this->condition->getPopulatedValue('remove') === '1'; + } + + /** + * Create first component of the escalation widget + * + * @return ?FlowLine|SubmitButtonElement + */ + protected function createFirstComponent() + { + if ($this->hasNoRemoveButton || $this->removeButton === null) { + return (new FlowLine())->getHorizontalLine(); + } + + return $this->removeButton; + } + + protected function assemble(): void + { + $firstComponent = $this->createFirstComponent(); + if ($firstComponent) { + $this->addHtml($firstComponent); + } + + $this->addHtml( + (new FlowLine())->getRightArrow(), + $this->condition, + (new FlowLine())->getRightArrow(), + $this->recipient + ); + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationConditionList.php b/library/Notifications/Widget/ItemList/EscalationConditionList.php new file mode 100644 index 00000000..ba503e57 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationConditionList.php @@ -0,0 +1,49 @@ + 'options']; + + protected $tag = 'ul'; + + /** @var EscalationConditionListItem[] Condition list items */ + protected $conditions; + + /** + * Create conditions list of the escalation + * + * @param EscalationConditionListItem[] $conditions + */ + public function __construct(array $conditions) + { + $this->conditions = $conditions; + } + + protected function assemble(): void + { + $removedPosition = null; + foreach ($this->conditions as $position => $condition) { + if ($condition->hasBeenRemoved()) { + $removedPosition = $position; + + continue; + } + + if ($removedPosition) { + $condition->setPosition($position - 1); + } + } + + foreach ($this->conditions as $position => $condition) { + if ($position !== $removedPosition) { + $this->addHtml($condition); + } + } + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationConditionListItem.php b/library/Notifications/Widget/ItemList/EscalationConditionListItem.php new file mode 100644 index 00000000..0c991794 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationConditionListItem.php @@ -0,0 +1,97 @@ + 'option']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button for the recipient */ + protected $removeButton; + + /** @var FormElement Condition type */ + protected $conditionType; + + /** @var FormElement Operator used for the condition */ + protected $operator; + + /** @var FormElement Condition value */ + protected $conditionVal; + + /** @var int */ + protected $position; + + /** + * Create the condition list item of the escalation + * + * @param FormElement $conditionType + * @param FormElement $operator + * @param FormElement $conditionVal + * @param ?SubmitButtonElement $removeButton + */ + public function __construct( + int $position, + FormElement $conditionType, + FormElement $operator, + FormElement $conditionVal, + ?SubmitButtonElement $removeButton + ) { + $this->position = $position; + $this->conditionType = $conditionType; + $this->operator = $operator; + $this->conditionVal = $conditionVal; + $this->removeButton = $removeButton; + } + + /** + * Return whether the condition has been removed + * + * @return bool + */ + public function hasBeenRemoved(): bool + { + return $this->removeButton && $this->removeButton->hasBeenPressed(); + } + + /** + * Set the position of the condition list item + * + * @param int $position + * + * @return $this + */ + public function setPosition(int $position): self + { + $this->position = $position; + + return $this; + } + + public function setRemoveButton(?SubmitButtonElement $removeButton): self + { + $this->removeButton = $removeButton; + + return $this; + } + + protected function assemble(): void + { + $this->conditionType->setAttribute('name', 'column_' . $this->position); + $this->operator->setAttribute('name', 'operator_' . $this->position); + $this->conditionVal->setAttribute('name', 'val_' . $this->position); + + $this->addHtml($this->conditionType, $this->operator, $this->conditionVal); + if ($this->removeButton) { + $this->removeButton->setSubmitValue((string) $this->position); + $this->addHtml($this->removeButton); + } + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationRecipientList.php b/library/Notifications/Widget/ItemList/EscalationRecipientList.php new file mode 100644 index 00000000..1c273580 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationRecipientList.php @@ -0,0 +1,55 @@ + 'options']; + + protected $tag = 'ul'; + + /** @var EscalationRecipientListItem[] Recipient list items of the escalation */ + protected $recipients; + + /** + * Create recipients list of the escalation + * + * @param EscalationRecipientListItem[] $recipients + */ + public function __construct(array $recipients) + { + $this->recipients = $recipients; + } + + protected function assemble(): void + { + $removedPosition = null; + $recipientCount = count($this->recipients); + foreach ($this->recipients as $position => $recipient) { + if ($recipient->hasBeenRemoved()) { + $removedPosition = $position; + --$recipientCount; + + continue; + } + + if ($removedPosition) { + $recipient->setPosition($position - 1); + } + } + + foreach ($this->recipients as $position => $recipient) { + if ($position !== $removedPosition) { + if ($recipientCount === 1) { + $recipient->removeRemoveButton(); + } + + $this->addHtml($recipient); + } + } + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php b/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php new file mode 100644 index 00000000..1e271a98 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php @@ -0,0 +1,92 @@ + 'option']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button for the recipient */ + protected $removeButton; + + /** @var FormElement Recipient name */ + protected $recipient; + + /** @var FormElement Recipient channel */ + protected $channel; + + /** @var int */ + protected $position; + + /** + * Create the recipient list item of the escalation + * + * @param int $position + * @param FormElement $recipient + * @param FormElement $channel + * @param ?SubmitButtonElement $removeButton + */ + public function __construct( + int $position, + FormElement $recipient, + FormElement $channel, + ?SubmitButtonElement $removeButton + ) { + $this->position = $position; + $this->recipient = $recipient; + $this->channel = $channel; + $this->removeButton = $removeButton; + } + + /** + * Return whether the condition has been removed + * + * @return bool + */ + public function hasBeenRemoved(): bool + { + return $this->removeButton && $this->removeButton->hasBeenPressed(); + } + + /** + * Set the position of the condition list item + * + * @param int $position + * + * @return $this + */ + public function setPosition(int $position): self + { + $this->position = $position; + + return $this; + } + + public function removeRemoveButton(): self + { + $this->removeButton = null; + + return $this; + } + + protected function assemble(): void + { + $this->recipient->setAttribute('name', 'column_' . $this->position); + $this->channel->setAttribute('name', 'val_' . $this->position); + + $this->addHtml($this->recipient, $this->channel); + if ($this->removeButton) { + $this->removeButton->setSubmitValue((string) $this->position); + + $this->addHtml($this->removeButton); + } + } +} diff --git a/library/Notifications/Widget/ItemList/Escalations.php b/library/Notifications/Widget/ItemList/Escalations.php new file mode 100644 index 00000000..c24f7ded --- /dev/null +++ b/library/Notifications/Widget/ItemList/Escalations.php @@ -0,0 +1,32 @@ + 'escalations']; + + protected $tag = 'ul'; + + /** @var Escalation[] Escalation list items */ + protected $escalations; + + /** + * Create the escalations list + * + * @param Escalation[] $escalations + */ + public function __construct(array $escalations) + { + $this->escalations = $escalations; + } + + protected function assemble(): void + { + $this->addHtml(...$this->escalations); + } +} diff --git a/phpstan-baseline-7x.neon b/phpstan-baseline-7x.neon index 5be0f381..96bfb457 100644 --- a/phpstan-baseline-7x.neon +++ b/phpstan-baseline-7x.neon @@ -5,11 +5,6 @@ parameters: count: 2 path: application/forms/EntryForm.php - - - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" - count: 2 - path: application/forms/EscalationRecipientForm.php - - message: "#^Parameter \\#1 \\$time of function strtotime expects string, mixed given\\.$#" count: 1 diff --git a/phpstan-baseline-8x.neon b/phpstan-baseline-8x.neon index 99ed68cf..0b138537 100644 --- a/phpstan-baseline-8x.neon +++ b/phpstan-baseline-8x.neon @@ -5,11 +5,6 @@ parameters: count: 2 path: application/forms/EntryForm.php - - - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" - count: 2 - path: application/forms/EscalationRecipientForm.php - - message: "#^Parameter \\#1 \\$datetime of function strtotime expects string, mixed given\\.$#" count: 1 diff --git a/phpstan-baseline-standard.neon b/phpstan-baseline-standard.neon index 7c04a796..bfb875b2 100644 --- a/phpstan-baseline-standard.neon +++ b/phpstan-baseline-standard.neon @@ -140,61 +140,6 @@ parameters: count: 1 path: application/controllers/EventRuleController.php - - - message: "#^Parameter \\#1 \\$id of static method Icinga\\\\Module\\\\Notifications\\\\Common\\\\Links\\:\\:eventRule\\(\\) expects int, mixed given\\.$#" - count: 3 - path: application/controllers/EventRuleController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:delete\\(\\) expects string, mixed given\\.$#" - count: 3 - path: application/controllers/EventRuleController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:get\\(\\) expects string, mixed given\\.$#" - count: 3 - path: application/controllers/EventRuleController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:set\\(\\) expects string, mixed given\\.$#" - count: 2 - path: application/controllers/EventRuleController.php - - - - message: "#^Parameter \\#1 \\$query of method ipl\\\\Web\\\\Control\\\\SearchEditor\\:\\:setQueryString\\(\\) expects string, mixed given\\.$#" - count: 1 - path: application/controllers/EventRuleController.php - - - - message: "#^Parameter \\#1 \\$ruleId of method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\EventRuleController\\:\\:fromDb\\(\\) expects int, mixed given\\.$#" - count: 2 - path: application/controllers/EventRuleController.php - - - - message: "#^Cannot access offset 'object_filter' on mixed\\.$#" - count: 2 - path: application/controllers/EventRulesController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:delete\\(\\) expects string, int given\\.$#" - count: 1 - path: application/controllers/EventRulesController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:get\\(\\) expects string, int given\\.$#" - count: 4 - path: application/controllers/EventRulesController.php - - - - message: "#^Parameter \\#1 \\$key of method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:set\\(\\) expects string, int given\\.$#" - count: 2 - path: application/controllers/EventRulesController.php - - - - message: "#^Parameter \\#1 \\$query of method ipl\\\\Web\\\\Control\\\\SearchEditor\\:\\:setQueryString\\(\\) expects string, mixed given\\.$#" - count: 1 - path: application/controllers/EventRulesController.php - - message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, int\\<1, max\\> given\\.$#" count: 1 @@ -231,19 +176,39 @@ parameters: path: application/controllers/ScheduleController.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\AddEscalationForm\\:\\:assemble\\(\\) has no return type specified\\.$#" + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\ScheduleController\\:\\:addAction\\(\\) has no return type specified\\.$#" count: 1 - path: application/forms/AddEscalationForm.php + path: application/controllers/ScheduleController.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\AddFilterForm\\:\\:assemble\\(\\) has no return type specified\\.$#" + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\ScheduleController\\:\\:addEntryAction\\(\\) has no return type specified\\.$#" count: 1 - path: application/forms/AddFilterForm.php + path: application/controllers/ScheduleController.php + + - + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\ScheduleController\\:\\:editEntryAction\\(\\) has no return type specified\\.$#" + count: 1 + path: application/controllers/ScheduleController.php + + - + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\ScheduleController\\:\\:indexAction\\(\\) has no return type specified\\.$#" + count: 1 + path: application/controllers/ScheduleController.php + + - + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\ScheduleController\\:\\:suggestRecipientAction\\(\\) has no return type specified\\.$#" + count: 1 + path: application/controllers/ScheduleController.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\BaseEscalationForm\\:\\:assemble\\(\\) has no return type specified\\.$#" + message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Controllers\\\\SchedulesController\\:\\:indexAction\\(\\) has no return type specified\\.$#" count: 1 - path: application/forms/BaseEscalationForm.php + path: application/controllers/SchedulesController.php + + - + message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, mixed given\\.$#" + count: 1 + path: application/controllers/SchedulesController.php - message: "#^Cannot call method getName\\(\\) on ipl\\\\Html\\\\Contract\\\\FormSubmitElement\\|null\\.$#" @@ -430,151 +395,6 @@ parameters: count: 1 path: application/forms/EntryForm.php - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setName\\(\\)\\.$#" - count: 4 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\ValidHtml\\:\\:remove\\(\\)\\.$#" - count: 1 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Call to an undefined method ipl\\\\Stdlib\\\\Filter\\\\Rule\\:\\:getColumn\\(\\)\\.$#" - count: 2 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Call to an undefined method ipl\\\\Stdlib\\\\Filter\\\\Rule\\:\\:getValue\\(\\)\\.$#" - count: 1 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Cannot access offset non\\-falsy\\-string on iterable\\\\.$#" - count: 2 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EscalationConditionForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EscalationConditionForm\\:\\:getValues\\(\\) should return array but returns string\\.$#" - count: 1 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Parameter \\#1 \\$string of static method ipl\\\\Web\\\\Filter\\\\QueryString\\:\\:parse\\(\\) expects string, mixed given\\.$#" - count: 1 - path: application/forms/EscalationConditionForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setDisabledOptions\\(\\)\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setName\\(\\)\\.$#" - count: 3 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setOptions\\(\\)\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\ValidHtml\\:\\:remove\\(\\)\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Cannot access offset non\\-falsy\\-string on iterable\\\\.$#" - count: 3 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Cannot access property \\$full_name on mixed\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Cannot access property \\$id on mixed\\.$#" - count: 3 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Cannot access property \\$name on mixed\\.$#" - count: 2 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EscalationRecipientForm\\:\\:fetchOptions\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EscalationRecipientForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/EscalationRecipientForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EventRuleForm\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/EventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\RemoveEscalationForm\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/RemoveEscalationForm.php - - - - message: "#^Call to an undefined method ipl\\\\Sql\\\\Connection\\:\\:lastInsertId\\(\\)\\.$#" - count: 3 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Cannot access property \\$id on mixed\\.$#" - count: 3 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:addRule\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:editRule\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:insertOrUpdateEscalations\\(\\) has parameter \\$escalations with no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:insertOrUpdateEscalations\\(\\) has parameter \\$ruleId with no type specified\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:onError\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Forms\\\\SaveEventRuleForm\\:\\:\\$submitLabel \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: application/forms/SaveEventRuleForm.php - - message: "#^Call to an undefined method ipl\\\\Sql\\\\Connection\\:\\:lastInsertId\\(\\)\\.$#" count: 1 @@ -645,16 +465,6 @@ parameters: count: 1 path: library/Notifications/Model/Channel.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Contact\\:\\:createBehaviors\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Model/Contact.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Contact\\:\\:createRelations\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Model/Contact.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Contact\\:\\:getColumnDefinitions\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -820,11 +630,6 @@ parameters: count: 1 path: library/Notifications/Model/RuleEscalation.php - - - message: "#^Cannot call method first\\(\\) on mixed\\.$#" - count: 3 - path: library/Notifications/Model/RuleEscalationRecipient.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\RuleEscalationRecipient\\:\\:createRelations\\(\\) has no return type specified\\.$#" count: 1 @@ -936,9 +741,9 @@ parameters: path: library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: library/Notifications/Web/Form/ContactForm.php + message: "#^Call to an undefined method ipl\\\\Sql\\\\Connection\\:\\:lastInsertId\\(\\)\\.$#" + count: 1 + path: library/Notifications/Web/Form/ContactForm.php - message: "#^Cannot access property \\$address on mixed\\.$#" @@ -1250,81 +1055,6 @@ parameters: count: 1 path: library/Notifications/Widget/Detail/IncidentQuickActions.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Escalations\\:\\:addEscalation\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Escalations.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Escalations\\:\\:addEscalation\\(\\) has parameter \\$escalation with no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Escalations.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Escalations\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Escalations.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Escalations\\:\\:\\$config has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Escalations.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Escalations\\:\\:\\$escalations has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Escalations.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:__construct\\(\\) has parameter \\$config with no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:createConditionForm\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:createFilterString\\(\\) has parameter \\$filters with no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:createRecipientForm\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:getConfig\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:getForms\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:setConfig\\(\\) has parameter \\$config with no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:\\$config type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventRuleConfig\\:\\:\\$numEscalations is unused\\.$#" - count: 1 - path: library/Notifications/Widget/EventRuleConfig.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\EventSourceBadge\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -1345,16 +1075,6 @@ parameters: count: 1 path: library/Notifications/Widget/FlowLine.php - - - message: "#^Cannot access offset 0 on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/ItemList/ContactListItem.php - - - - message: "#^Parameter \\#1 \\$content of static method ipl\\\\Html\\\\Text\\:\\:create\\(\\) expects string, mixed given\\.$#" - count: 1 - path: library/Notifications/Widget/ItemList/ContactListItem.php - - message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, int given\\.$#" count: 1 diff --git a/public/css/detail/event-rule-detail.less b/public/css/detail/event-rule-detail.less index 6f85b48d..0e3c83e1 100644 --- a/public/css/detail/event-rule-detail.less +++ b/public/css/detail/event-rule-detail.less @@ -1,204 +1,6 @@ .event-rule-detail { display: flex; align-items: baseline; - - > .right-arrow:first-child { - margin-top: 3.125em; - } - - &.invalid { - .escalations .escalation form.escalation-form { - select, - input { - &:invalid { - background-color: red; - } - } - } - } - - .search-controls { - display: inline-flex; - width: 20em; - min-width: unset; - padding: 0.5em; - border: 1px solid @gray-lighter; - border-radius: 0.5em; - - input.filter-input { - width: 20em; - background-color: @search-term-bg; - color: @search-term-color; - } - } - - .escalations { - display: inline-flex; - flex-direction: column; - width: 70em; - - .vertical-line { - position: absolute; - z-index: -1; - top: 15%; - bottom: 0; - margin-left: 1.25em; - } - - > .escalation { - display: flex; - align-items: center; - padding-bottom: 2em; - position: relative; - - &:before { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - width: .5em; - margin-left: 1.25em; - background: @gray-lighter; - z-index: -1; - } - - &:first-child:before { - content: ""; - display: block; - top: calc(~"50% - 1em"); - } - - .right-arrow:first-child { - width: 2em; - } - - .right-arrow-long { - width: 38em; - } - - .right-arrow.right-arrow-long:first-child { - width: 47em; - } - - .right-arrow-one-escalation:first-child { - width: 15em; - } - - .escalation-condition-form, - .escalation-recipient-form { - width: 100%; - - padding: 0.5em; - border: 1px solid @gray-lighter; - border-radius: 0.5em; - - .options { - list-style-type: none; - padding: 0; - margin: 0; - - > li { - display: flex; - margin-bottom: .4em; - - &.option { - .errors { - display: inline-flex; - width: fit-content; - margin: 0; - } - - .errors + .remove-button { - margin: 0; - } - } - } - - .default-channel { - color: @disabled-gray; - } - - select, input { - min-width: 10em; - text-align: center; - height: 2.25em; - line-height: normal; - background: @search-term-bg; - color: @search-term-color; - } - - select { - background-image: url('@{iplWebAssets}/img/select-icon.svg'); - background-position: center right; - background-repeat: no-repeat; - } - - .left-operand { - border-radius: 0.4em 0 0 0.4em; - margin-right: 1px; - } - - .right-operand { - border-radius: 0 0.4em 0.4em 0; - width: 0; - flex: 1 1 auto; - margin-left: 1px; - } - - .operator-input { - min-width: unset; - padding-right: 0.5em; - width: 3em; - border-radius: unset; - margin: 0 1px; - background: @search-term-bg; - color: @search-term-color; - - option { text-align: center; } - } - } - - .remove-button { - height: 2.25em; - margin-left: 0.5em; - } - } - - .escalation-condition-form.count-zero-escalation-condition-form { - width: fit-content; - border: none; - margin: 0; - padding: 0; - - button[type="submit"] { - font-size: 2em; - width: 3em; - margin: 0; - background: @low-sat-blue; - border: none; - - &:hover { - background: @low-sat-blue-dark; - } - } - } - - .escalation-condition-form.count-zero-escalation-condition-form:after { - content: 'Condition'; - align-self: center; - margin-bottom: -1.5em; - color: @text-color-light; - } - } - } -} - -.escalations-with-add-form { - .add-escalation-form { - position: relative; - margin-left: -0.1em; - } } .cache-notice { @@ -209,228 +11,58 @@ .rounded-corners(); } -// Collecting button styles -.event-rule-button() { - color: @icinga-blue; - background: @low-sat-blue; - - border: none; - text-align: center; - line-height: 1.5; - display: block; - - &:hover, - &:focus { - color: @icinga-blue; - } - - &:hover { - background: @low-sat-blue-dark; - } - - &:focus { - outline: 3px solid fade(@icinga-blue, 50%); - outline-offset: 1px; - } +.new-event-rule { + margin-bottom: 1em; } -.escalation-form { - display: flex; - flex-direction: column; +.event-rule-form { + display: inline-flex; + width: fit-content; + max-width: unset; + align-items: flex-start; - .options +.add-button, - .remove-button { - .event-rule-button(); + > h2 { + margin: 0 0 0.5em 0; } - .options + .add-button { - margin-right: 3.5em; - } + .control-group { + display: inline-flex; + margin-right: 2em; - .options li { - input, select { - &:last-child:not(.remove-button) { - margin-right: 3.5em; - } + .control-label-group { + width: auto; } - } -} - -.add-filter-form, -.escalation-form.count-zero-escalation-condition-form { - button[type="submit"] { - font-size: 2em; - height: 2.25em; - margin: 0; - > .icon { - flex-wrap: wrap; - align-content: flex-start; + input[type='text'] { + max-width: unset; + width: 25em; } - - .event-rule-button(); } } -.remove-escalation-form button[type="submit"], -.add-escalation-form button[type="submit"] { - .event-rule-button(); -} - -.right-arrow, -.horizontal-line { - display: inline-block; - background-color: @base-gray-lighter; - height: 0.5em; - text-align: end; -} - -.right-arrow { - width: 10em; - min-width: 2em; - margin-right: 0.4em; - position: relative; -} - -.horizontal-line { - width: 3em; - min-width: 1em; -} - -.right-arrow:after { - content: ''; - position: absolute; - border: 0.3em solid transparent; - border-left: 0.4em solid @base-gray-lighter; -} - -.vertical-line { - width: 0.7em; - background-color: @base-gray-lighter; -} - -.remove-escalation-form { +.save-config { + display: inline-flex; + float: right; width: fit-content; -} - -#layout.minimal-layout form.icinga-form:not(.inline).remove-escalation-form:not(.inline), -#layout.twocols:not(.wide-layout) form.icinga-form.remove-escalation-form:not(.inline) { - width: fit-content; -} + flex-direction: row-reverse; -.add-escalation-form { - display: flex; - min-height: 6em; - align-items: center; - - &:before { - content: ""; - display: block; - position: absolute; - width: .5em; - background: @gray-lighter; - top: 0; - bottom: 50%; - left: calc(~"1.25em + 1px"); - z-index: -1; - } -} - -#layout.minimal-layout form.icinga-form:not(.inline).count-zero-escalation-condition-form:not(.inline), -#layout.twocols:not(.wide-layout) form.icinga-form.escalation-condition-form.count-zero-escalation-condition-form:not(.inline) { - width: fit-content; -} - -.add-filter-form { - text-align: center; - width: auto; - position: relative; - bottom: calc(~"-.5em - 1px"); -} - -.add-filter-form:after { - content: 'Filter'; - display: block; - color: @text-color-light; -} - -.horizontal-line-long { - width: 14.5em; -} - -.new-event-rule { - margin-bottom: 1em; -} - -.event-rule-and-save-forms { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - padding-bottom: 0.75em; - - .event-rule-form { - display: inline-flex; - width: fit-content; - max-width: unset; - - .control-group { - display: inline-flex; - margin-right: 2em; - - :last-child { - float: right; - } - - .control-label-group { - width: auto; - } + button[type="submit"] { + margin-right: 1em; - input[type='text'] { - max-width: unset; - width: 25em; - } + &.btn-remove:not([disabled]) { + .button(@body-bg-color, @color-critical, @color-critical-accentuated); + border: none; } - } - - .save-event-rule { - height: 2.25em; - display: inline-flex; - float: right; - margin: 1em 0 0 auto; - - input[type="submit"]:not(:first-child) { - margin-left: 1em; - - &:disabled { - background: @gray-light; - color: @disabled-gray; - cursor: not-allowed; - border-color: transparent; - } - - &.btn-remove { - border: none; - &:disabled { - background: none; - cursor: not-allowed; - opacity: 0.5; - } - } - - &.btn-discard-changes { - .event-rule-button(); - } + &.btn-discard-changes { + .event-rule-button(); } - } -} -.remove-escalation-form { - button[disabled] { &:disabled { background: @gray-light; color: @disabled-gray; cursor: not-allowed; + border: transparent; } } } diff --git a/public/css/event-rule-config.less b/public/css/event-rule-config.less new file mode 100644 index 00000000..eb886b33 --- /dev/null +++ b/public/css/event-rule-config.less @@ -0,0 +1,328 @@ +.event-rule-config { + display: flex; + align-items: center; + ul { + list-style-type: none; + margin: 0; + li { + display: inline-flex; + align-items: center; + } + } + + .escalations { + padding: 0; + position: relative; + + > .escalation:first-child:before { + content: ""; + display: block; + top: 2em; + } + + > .escalation:before { + content: ""; + display: block; + position: absolute; + top: 1.25em; + bottom: 0; + width: 0.5em; + margin-left: 1.25em; + background: var(--gray-lighter, #4b4b4b); + z-index: -1; + } + } + + .config-filter + .add-button { + align-self: flex-end; + } + + .filter-wrapper { + display: inline-flex; + align-self: flex-start; + } + + .filter-wrapper:has(.config-filter .search-controls) { + align-items: baseline; + } + + .add-escalation { + width: fit-content; + display: block; + } + + .right-arrow, + .horizontal-line { + display: inline-block; + background-color: @base-gray-lighter; + height: 0.5em; + text-align: end; + } + + .right-arrow { + width: 10em; + min-width: 2em; + margin-right: 0.4em; + position: relative; + } + + .horizontal-line { + width: 3em; + min-width: 1em; + } + + .right-arrow:after { + content: ''; + position: absolute; + border: 0.3em solid transparent; + border-left: 0.4em solid @base-gray-lighter; + } + + .vertical-line { + width: 0.7em; + background-color: @base-gray-lighter; + } + + .escalation { + margin-bottom: 2em; + .remove-button { + align-self: flex-start; + + &:disabled { + background: @gray-light; + color: @disabled-gray; + cursor: not-allowed; + border-color: transparent; + } + } + + .horizontal-line { + min-width: 3.5em; + } + + .right-arrow { + min-width: 10em; + } + + .zero-escalation-condition + .right-arrow, + .right-arrow:has(+ .zero-escalation-condition) { + min-width: calc(~"40% - 0.85em"); + } + + .remove-escalation { + margin-top: 1.25em; + } + } + + .config-filter { + align-self: flex-start; + padding-top: 0.5em; + } + + .search-controls { + display: inline-flex; + width: 20em; + min-width: unset; + padding: 0.5em; + border: 1px solid @gray-lighter; + border-radius: 0.5em; + + input.filter-input { + width: 20em; + background-color: @search-term-bg; + color: @search-term-color; + } + } +} + +.filter-wrapper, +.escalations { + .horizontal-line, + .right-arrow { + margin-top: 2em; + align-self: flex-start; + } +} + +.horizontal-line { + min-width: 10em; +} + +.remove-button, +.add-button { + .event-rule-button(); +} + +.escalation-condition, +.escalation-recipient { + width: 100%; + padding: 0.5em; + border: 1px solid @gray-lighter; + border-radius: 0.5em; + align-self: flex-start; + + .options { + list-style-type: none; + padding: 0; + margin: 0; + + > li { + display: flex; + margin-bottom: .4em; + + &.option { + .errors { + display: inline-flex; + width: fit-content; + margin: 0; + } + + .errors + .remove-button { + margin: 0; + } + } + } + + .default-channel { + color: @disabled-gray; + } + + select, input { + min-width: 10em; + text-align-last: center; // text-align does not work in safari for select tags + height: 2.25em; + line-height: normal; + background: @search-term-bg; + color: @search-term-color; + } + + select { + background-image: url('@{iplWebAssets}/img/select-icon.svg'); + background-position: center right; + background-repeat: no-repeat; + } + + .left-operand { + border-radius: 0.4em 0 0 0.4em; + } + + .right-operand { + border-radius: 0 0.4em 0.4em 0; + width: 0; + flex: 1 1 auto; + margin-left: 1px; + } + + .operator-input { + min-width: unset; + padding-right: 0.5em; + width: 3em; + border-radius: unset; + margin: 0 1px; + background: @search-term-bg; + color: @search-term-color; + + option { text-align: center; } + } + } + + .remove-button { + height: 2.25em; + margin-left: 0.5em; + } + + input::-webkit-calendar-picker-indicator { + display: none; + } +} + +.escalation-recipient .left-operand { + margin-right: 1px; +} + +.escalation-condition, +.escalation-recipient { + .options + .add-button, + .remove-button { + .event-rule-button(); + } + + .options + .add-button { + width: calc(~"100% - 3.5em"); + } + + .options li { + input, select { + &:last-child:not(.remove-button) { + margin-right: 3.5em; + } + } + } +} + +.config-filter.empty-filter, +.escalation-condition.zero-escalation-condition { + border: 0; + padding: 0; + button[type="submit"] { + width: 100%; + font-size: 2em; + height: 2.25em; + margin: 0; + + > .icon { + flex-wrap: wrap; + align-content: flex-start; + } + + .event-rule-button(); + } +} + +.config-filter.empty-filter:after { + content: 'Filter'; + display: block; + text-align: center; + color: @text-color-light; +} + +.zero-escalation-condition:after { + content: 'Condition'; + display: block; + text-align: center; + color: @text-color-light; +} + +.event-rule-button() { + color: @icinga-blue; + background: @low-sat-blue; + + border: none; + text-align: center; + line-height: 1.5; + display: block; + + &:hover, + &:focus { + color: @icinga-blue; + } + + &:hover { + background: @low-sat-blue-dark; + } + + &:focus { + outline: 3px solid fade(@icinga-blue, 50%); + outline-offset: 1px; + } +} + +.submit-btn-duplicate { + border: 0; + height: 0; + margin: 0; + padding: 0; + visibility: hidden; + width: 0; + position: absolute; +}