diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index f52b59ad0..51d4d1dd7 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -7,20 +7,24 @@ 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\Module\Notifications\Web\Form\EventRuleDecorator; use Icinga\Web\Notification; use Icinga\Web\Session; +use ipl\Html\Attributes; use ipl\Html\Form; +use ipl\Html\FormElement\ButtonElement; +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; @@ -32,6 +36,9 @@ class EventRuleController extends CompatController /** @var Session\SessionNamespace */ private $sessionNamespace; + /** @var ?string Event rule config filter */ + protected $filter; + public function init() { $this->sessionNamespace = Session::getSession()->getNamespace('notifications'); @@ -42,71 +49,105 @@ public function indexAction(): void $this->assertPermission('notifications/config/event-rules'); $this->addTitleTab(t('Event Rule')); - $this->controls->addAttributes(['class' => 'event-rule-detail']); + /** @var int $ruleId */ $ruleId = $this->params->getRequired('id'); + /** @var array|null $config */ + $config = $this->sessionNamespace->get((string) $ruleId); + $this->controls->addAttributes(['class' => 'event-rule-detail']); - $cache = $this->sessionNamespace->get($ruleId); + $discardChangesButton = null; + if ($config === null) { + $config = $this->fromDb($ruleId); + } + + $eventRuleConfig = (new EventRuleConfigForm( + $config, + Url::fromPath( + 'notifications/event-rule/search-editor', + ['id' => $config['id']] + ) + ))->populate($config); + $eventRuleConfig + ->on(Form::ON_SUCCESS, function (EventRuleConfigForm $form) use ($config) { + /** @var string $ruleId */ + $ruleId = $config['id']; + $form->insertOrAddRule($ruleId, $config); + $this->sessionNamespace->delete($ruleId); + Notification::success((sprintf(t('Successfully saved event rule %s'), $config['name']))); - if ($cache) { + $this->sendExtraUpdates(['#col1']); + $this->redirectNow(Links::eventRule((int) $ruleId)); + }) + ->on(EventRuleConfigForm::ON_DELETE, function (EventRuleConfigForm $form) use ($config) { + $ruleId = $config['id']; + $form->removeRule($ruleId); + $this->sessionNamespace->delete($ruleId); + Notification::success(sprintf(t('Successfully deleted event rule %s'), $config['name'])); + $this->redirectNow('__CLOSE__'); + }) + ->on(EventRuleConfigForm::ON_DISCARD, function () use ($config) { + $ruleId = $config['id']; + $this->sessionNamespace->delete($ruleId); + Notification::success(sprintf(t('Successfully discarded changes to event rule %s'), $config['name'])); + $this->redirectNow(Links::eventRule((int) $ruleId)); + }) + ->on(EventRuleConfigForm::ON_CHANGE, function (EventRuleConfigForm $form) use ($config) { + $config = array_merge($config, $form->getValues()); + $this->sessionNamespace->set($config['id'], $config); + }) + ->handleRequest($this->getServerRequest()); + + $cache = $this->sessionNamespace->get((string) $ruleId); + 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, + ] + )); } - $disableRemoveButton = false; - if (ctype_digit($ruleId)) { + + $buttonsWrapper = new HtmlElement('div', Attributes::create(['class' => ['icinga-controls', 'save-config']])); + $eventRuleConfigSubmitButton = (new SubmitButtonElement( + 'save', + [ + 'label' => t('Save'), + 'form' => 'event-rule-config-form' + ] + )); + $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) { $incidents = Incident::on(Database::get()) ->with('rule') ->filter(Filter::equal('rule.id', $ruleId)); if ($incidents->count() > 0) { - $disableRemoveButton = true; + $deleteButton->addAttributes(['disabled' => true]); } } - $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', $config['name'] ?? ''), (new Link( new Icon('edit'), Url::fromPath('notifications/event-rule/edit', [ @@ -115,30 +156,9 @@ public function indexAction(): void ['class' => 'control-button'] ))->openInModal() ]); + $this->addControl($eventRuleForm); - $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($buttonsWrapper); $this->addContent($eventRuleConfig); } @@ -167,7 +187,7 @@ public function fromDb(int $ruleId): array } foreach ($re->rule_escalation_recipient as $recipient) { - $config[$re->getTableName()][$re->position]['recipient'][] = iterator_to_array($recipient); + $config[$re->getTableName()][$re->position]['recipients'][] = iterator_to_array($recipient); } } @@ -188,7 +208,6 @@ public function completeAction(): void $this->getDocument()->add($suggestions); } - /** * searchEditorAction for Object Extra Tags * @@ -198,16 +217,29 @@ 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(); + /** @var string $objectFilter */ + $objectFilter = $eventRule['object_filter'] ?? ''; + $editor->setQueryString($objectFilter); + $editor->setAction(Url::fromRequest()->getAbsoluteUrl()); + $editor->setSuggestionUrl(Url::fromPath( + "notifications/event-rule/complete", + ['_disableLayout' => true, 'showCompact' => true, 'id' => Url::fromRequest()->getParams()->get('id')] + )); + + $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($ruleId, $eventRule) { + $filter = self::createFilterString($form->getFilter()); + $eventRule['object_filter'] = $filter; $this->sessionNamespace->set($ruleId, $eventRule); $this->getResponse() ->setHeader('X-Icinga-Container', '_self') @@ -225,20 +257,35 @@ public function searchEditorAction(): 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 ! empty($filterStr) ? $filterStr : ''; + } + 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 ?? []; + if ($ruleId === '-1') { + $config = ['id' => $ruleId]; } else { $config = $this->fromDb((int) $ruleId); } @@ -246,27 +293,24 @@ public function editAction(): void $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); + $params = []; + if ($ruleId === '-1') { + $params = $config; } else { - (new SaveEventRuleForm())->editRule((int) $ruleId, $config); + $params['id'] = $ruleId; } if ($ruleId === '-1') { - $redirectUrl = Url::fromPath('notifications/event-rules/add', [ - 'use_cache' => true - ]); + $redirectUrl = Url::fromPath('notifications/event-rules/add', $params); } else { - $redirectUrl = Url::fromPath('notifications/event-rule', [ - 'id' => $ruleId - ]); + $redirectUrl = Url::fromPath('notifications/event-rule', $params); $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 73e46db8e..8f8334d93 100644 --- a/application/controllers/EventRulesController.php +++ b/application/controllers/EventRulesController.php @@ -4,26 +4,23 @@ namespace Icinga\Module\Notifications\Controllers; -use Icinga\Exception\ProgrammingError; use Icinga\Module\Notifications\Common\Database; -use Icinga\Module\Notifications\Common\Links; -use Icinga\Module\Notifications\Forms\EventRuleForm; -use Icinga\Module\Notifications\Forms\SaveEventRuleForm; -use Icinga\Module\Notifications\Model\ObjectExtraTag; +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; @@ -88,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']) @@ -110,19 +107,56 @@ public function addAction(): void $this->getTabs()->setRefreshUrl(Url::fromPath('notifications/event-rules/add')); $this->controls->addAttributes(['class' => 'event-rule-detail']); + /** @var string $ruleId */ + $ruleId = $this->params->getRequired('id'); - if ($this->params->has('use_cache') || $this->getServerRequest()->getMethod() !== 'GET') { - $cache = $this->sessionNamespace->get(-1, []); - } else { - $this->sessionNamespace->delete(-1); + $params = $this->params->toArray(false); + /** @var array|null $config */ + $config = $this->sessionNamespace->get($ruleId); - $cache = []; + if ($config === null) { + /** @var array $config */ + $config = $params; } - $eventRuleConfig = new EventRuleConfig(Url::fromPath('notifications/event-rules/add-search-editor'), $cache); + $eventRuleConfigSubmitButton = (new SubmitButtonElement( + 'save', + [ + 'label' => t('Save'), + 'form' => 'event-rule-config-form', + 'formnovalidate' => true + ] + ))->setWrapper(new HtmlElement('div', Attributes::create(['class' => ['icinga-controls', 'save-config']]))); + + $eventRuleConfig = (new EventRuleConfigForm( + $config, + Url::fromPath( + 'notifications/event-rules/search-editor', + ['id' => $ruleId] + ) + )) + ->registerElement($eventRuleConfigSubmitButton) + ->populate($config); + + $eventRuleConfig + ->on(Form::ON_SENT, function (Form $form) use ($config) { + $config = array_merge($config, $form->getValues()); + $this->sessionNamespace->set('-1', $config); + }) + ->on(Form::ON_SUCCESS, function (EventRuleConfigForm $form) use ($config) { + /** @var string $ruleId */ + $ruleId = $config['id']; + /** @var string $ruleName */ + $ruleName = $config['name']; + $form->insertOrAddRule($ruleId, $config); + $this->sessionNamespace->delete($ruleId); + Notification::success(sprintf(t('Successfully add event rule %s'), $ruleName)); + $this->redirectNow('__CLOSE__'); + }) + ->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', [ @@ -132,42 +166,8 @@ 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); } @@ -181,47 +181,70 @@ public function completeAction(): void public function searchEditorAction(): void { - $editor = $this->createSearchEditor( - Rule::on(Database::get()), - [ - LimitControl::DEFAULT_LIMIT_PARAM, - SortControl::DEFAULT_SORT_PARAM, - ] - ); + /** @var string $ruleId */ + $ruleId = $this->params->shiftRequired('id'); - $this->getDocument()->add($editor); - $this->setTitle($this->translate('Adjust Filter')); - } + /** @var array|null $eventRule */ + $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()); + /** @var string $objectFilter */ + $objectFilter = $eventRule['object_filter'] ?? ''; + $editor->setQueryString($objectFilter); + $editor->setAction(Url::fromRequest()->getAbsoluteUrl()); + $editor->setSuggestionUrl(Url::fromPath( + "notifications/event-rule/complete", + ['_disableLayout' => true, 'showCompact' => true, 'id' => Url::fromRequest()->getParams()->get('id')] + )); - $this->sessionNamespace->set(-1, $cache); + $editor->on(SearchEditor::ON_SUCCESS, function (SearchEditor $form) use ($ruleId, $eventRule) { + $filter = self::createFilterString($form->getFilter()); + $eventRule['object_filter'] = $filter; + $this->sessionNamespace->set($ruleId, $eventRule); $this->getResponse() ->setHeader('X-Icinga-Container', '_self') ->redirectAndExit( Url::fromPath( 'notifications/event-rules/add', - ['use_cache' => true] + ['id' => $ruleId] ) ); }); $editor->handleRequest($this->getServerRequest()); - $this->getDocument()->addHtml($editor); + $this->getDocument()->add($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) { + self::createFilterString($filter); + } + } elseif ($filters instanceof Filter\Condition && empty($filters->getValue())) { + $filters->setValue(true); + } + + $filterStr = QueryString::render($filters); + + return ! empty($filterStr) ? $filterStr : ''; + } + /** * Get the filter created from query string parameters * diff --git a/application/forms/EventRuleConfigElements/EscalationCondition.php b/application/forms/EventRuleConfigElements/EscalationCondition.php new file mode 100644 index 000000000..9efcab792 --- /dev/null +++ b/application/forms/EventRuleConfigElements/EscalationCondition.php @@ -0,0 +1,311 @@ + 'escalation-condition']; + + /** @var EscalationConditionListItem[] Condition list */ + protected $conditions = []; + + /** @var bool Whether zero conditions allowed */ + public $allowZeroConditions; + + /** @var int Number of conditions */ + public $count = 0; + + /** + * Set whether the zero conditions is allowed for the escalation + * + * @param bool $allowZeroConditions + * + * @return $this + */ + public function setAllowZeroConditions(bool $allowZeroConditions): self + { + $this->allowZeroConditions = $allowZeroConditions; + + return $this; + } + + protected function assemble(): void + { + if ($this->allowZeroConditions) { + $defaultCount = 0; + } else { + $defaultCount = 1; + } + + $this->addElement( + 'hidden', + 'condition-count', + ['value' => (string) $defaultCount] + ); + + /** @var SubmitButtonElement $addCondition */ + $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); + + /** @var int $conditionCount */ + $conditionCount = $this->getValue('condition-count'); + $this->addElement( + 'hidden', + 'id' + ); + + if ($addCondition->hasBeenPressed()) { + $conditionCount += 1; + $this->getElement('condition-count')->setValue($conditionCount); + } + + $removePosition = null; + for ($i = 1; $i <= $conditionCount; $i++) { + $colName = 'column_' . $i; + $opName = 'operator_' . $i; + $typeName = 'type_' . $i; + $valName = 'val_' . $i; + + /** @var BaseFormElement $col */ + $col = $this->createElement( + 'select', + $colName, + [ + '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 = ['=', '>', '>=', '<', '<=', '!=']; + /** @var BaseFormElement $op */ + $op = $this->createElement( + 'select', + $opName, + [ + 'class' => ['class' => 'operator-input', 'autosubmit'], + 'options' => array_combine($operators, $operators), + 'required' => true + ] + ); + + switch ($this->getPopulatedValue('column_' . $i)) { + case 'incident_severity': + /** @var BaseFormElement $val */ + $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') + ] + ] + ); + + if ( + $this->getPopulatedValue($typeName) !== 'incident_severity' + && $this->getPopulatedValue($valName) !== null + ) { + $this->clearPopulatedValue($typeName); + $this->clearPopulatedValue($valName); + } + + $this->addElement('hidden', $typeName, [ + 'ignore' => true, + 'value' => 'incident_severity' + ]); + + break; + case 'incident_age': + /** @var BaseFormElement $val */ + $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; + })] + ] + ); + + if ( + $this->getPopulatedValue($typeName) !== 'incident_age' + && $this->getPopulatedValue($valName) !== null + ) { + $this->clearPopulatedValue($typeName); + $this->clearPopulatedValue($valName); + } + + $this->addElement('hidden', $typeName, [ + 'ignore' => true, + 'value' => 'incident_age' + ]); + + break; + default: + /** @var BaseFormElement $val */ + $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); + + (new EventRuleDecorator())->decorate($val); + /** @var ?SubmitButtonElement $removeButton */ + $removeButton = $this->createRemoveButton($i); + if ($removeButton && $removeButton->hasBeenPressed()) { + $removePosition = $i; + } + + $this->conditions[$i] = new EscalationConditionListItem( + $col, + $op, + $val, + $removeButton + ); + } + + if ($removePosition) { + unset($this->conditions[$removePosition]); + $conditionCount -= 1; + if ($conditionCount === 1 && ! $this->allowZeroConditions && $removePosition === 2) { + $this->conditions[1]->removeButton = null; + } else { + for ($n = $removePosition; $n <= $conditionCount; $n++) { + $nextCount = $n + 1; + $this->conditions[$nextCount]->conditionType->setName('column_' . $n); + $this->conditions[$nextCount]->operator->setName('operator_' . $n); + $this->conditions[$nextCount]->conditionVal->setName('val_' . $n); + if ($conditionCount === 1) { + $this->conditions[$nextCount]->removeButton = null; + } elseif ($this->conditions[$nextCount]->removeButton) { + $this->conditions[$nextCount]->removeButton->setName('remove_' . $n); + } + } + } + $this->getElement('condition-count')->setValue($conditionCount); + } + + if ((int) $conditionCount === 0) { + $this->addAttributes(['class' => ['zero-escalation-condition']]); + } elseif ($this->getAttributes()) { + $this->getAttributes()->remove('class', 'zero-escalation-condition'); + } + + $this->add(new EscalationConditionList($this->conditions)); + + $this->addElement($addCondition); + } + + protected function createRemoveButton(int $count): ?SubmitButtonElement + { + // check for count and if allow zero conditions + /** @var string|int $conditionCount */ + $conditionCount = $this->getValue('condition-count'); + if ((int) $conditionCount === 1 && ! $this->allowZeroConditions) { + return null; + } + + /** @var SubmitButtonElement $removeButton */ + $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; + } + + public function hasValue(): bool + { + $this->ensureAssembled(); + return parent::hasValue(); + } + + public function getCondition(): string + { + $filter = Filter::any(); + /** @var int $count */ + $count = $this->getValue('condition-count'); + + if ($count > 0) { // if count is 0, loop runs in reverse direction + foreach (range(1, $count) as $count) { + $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 000000000..752a28775 --- /dev/null +++ b/application/forms/EventRuleConfigElements/EscalationRecipient.php @@ -0,0 +1,268 @@ + 'escalation-recipient']; + + /** @var EscalationRecipientListItem[] */ + protected $recipients = []; + + protected function assemble(): void + { + $this->addElement( + 'hidden', + 'recipient-count', + ['value' => '1'] + ); + + /** @var SubmitButtonElement $addRecipientButton */ + $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); + /** @var int $recipientCount */ + $recipientCount = $this->getValue('recipient-count'); + if ($addRecipientButton->hasBeenPressed()) { + $recipientCount += 1; + $this->getElement('recipient-count')->setValue($recipientCount); + } + + $removePosition = null; + foreach (range(1, $recipientCount) as $i) { + $this->addElement( + 'hidden', + 'id_' . $i + ); + + /** @var BaseFormElement $col */ + $col = $this->createElement( + 'select', + 'column_' . $i, + [ + 'class' => ['autosubmit', 'left-operand'], + 'options' => [ + '' => sprintf(' - %s - ', $this->translate('Please choose')) + ] + $this->fetchOptions(), + 'disabledOptions' => [''], + 'required' => true, + 'value' => $this->getPopulatedValue('column_' . $i) + ] + ); + + $this->registerElement($col); + + $options = ['' => sprintf(' - %s - ', $this->translate('Please choose'))]; + $options += Channel::fetchChannelNames(Database::get()); + + /** @var SelectElement $val */ + $val = $this->createElement( + 'select', + 'val_' . $i, + [ + 'class' => ['autosubmit', 'right-operand'], + 'options' => $options, + 'disabledOptions' => [''], + 'value' => $this->getPopulatedValue('val_' . $i) + ] + ); + + /** @var string $recipientVal */ + $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 { + /** @var BaseFormElement $val */ + $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); + + /** @var ?SubmitButtonElement $removeButton */ + $removeButton = $this->createRemoveButton($i); + + if ($removeButton && $removeButton->hasBeenPressed()) { + $removePosition = $i; + } + + $this->recipients[$i] = new EscalationRecipientListItem( + $col, + $val, + $removeButton + ); + } + + if ($removePosition) { + unset($this->recipients[$removePosition]); + $recipientCount -= 1; + if ($recipientCount === 1 && $removePosition === 2) { + $this->recipients[1]->removeButton = null; + } else { + for ($n = $removePosition; $n <= $recipientCount; $n++) { + $nextCount = $n + 1; + $this->recipients[$nextCount]->recipient->setName('column_' . $n); + $this->recipients[$nextCount]->channel->setName('val_' . $n); + if ($recipientCount === 1) { + $this->recipients[$nextCount]->removeButton = null; + } elseif ($this->recipients[$nextCount]->removeButton) { + $this->recipients[$nextCount]->removeButton->setName('remove_' . $n); + } + } + } + + $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 FormElement|null + */ + protected function createRemoveButton(int $pos): ?FormElement + { + /** @var string|int $recipientCount */ + $recipientCount = $this->getValue('recipient-count'); + if ((int) $recipientCount === 1) { + return null; + } + + $removeButton = $this->createElement( + 'submitButton', + 'remove_' . $pos, + [ + 'class' => ['remove-button', 'control-button', 'spinner'], + 'label' => new Icon('minus'), + 'title' => $this->translate('Remove'), + 'formnovalidate' => true + ] + ); + + $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'); + + /** @var array> $values */ + $values = []; + for ($i = 1; $i <= $count; $i++) { + $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; + } + + public function renderUnwrapped() + { + $this->ensureAssembled(); + + if ($this->isEmpty()) { + return ''; + } + + return parent::renderUnwrapped(); + } +} diff --git a/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php b/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php new file mode 100644 index 000000000..e1db8abf0 --- /dev/null +++ b/application/forms/EventRuleConfigElements/EventRuleConfigFilter.php @@ -0,0 +1,130 @@ + 'config-filter']; + + protected function assemble(): void + { + $this->addElement( + 'hidden', + 'show-searchbar', + ['value' => 0] + ); + + /** @var SubmitButtonElement $addFilterButton */ + $addFilterButton = $this->createElement( + 'submitButton', + 'add-filter', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'formnovalidate' => true, + 'title' => $this->translate('Add filter') + ] + ); + + $this->registerElement($addFilterButton); + /** @var int $showSearchBar */ + $showSearchBar = $this->getValue('show-searchbar'); + if ($this->objectFilter !== '' || $addFilterButton->hasBeenPressed()) { + $showSearchBar = 1; + $this->getElement('show-searchbar')->setValue($showSearchBar); + } + + if ($showSearchBar === 0) { + /** @var SubmitButtonElement $filterElement */ + $filterElement = $addFilterButton; + } else { + $editorOpener = new Link( + new Icon('cog'), + $this->getSearchEditorUrl(), + 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' => $this->objectFilter + ] + ); + + $filterElement = Html::tag('div', ['class' => 'search-controls icinga-controls']); + $filterElement->add([$searchBar, $editorOpener]); + } + + $this->add($filterElement); + } + + /** + * Set the Url of the search editor + * + * @param Url $url + * + * @return $this + */ + public function setSearchEditorUrl(Url $url): self + { + $this->searchEditorUrl = $url; + + return $this; + } + + /** + * Get the search editor's Url + * + * @return Url + */ + public function getSearchEditorUrl(): Url + { + return $this->searchEditorUrl; + } + + /** + * Set the event rule's object filter + * + * @param string $filter + * + * @return $this + */ + public function setObjectFilter(string $filter): self + { + $this->objectFilter = rawurldecode($filter); + + return $this; + } + + /** + * 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 000000000..676707da4 --- /dev/null +++ b/application/forms/EventRuleConfigForm.php @@ -0,0 +1,712 @@ + ['event-rule-config', 'icinga-form', 'icinga-controls'], + 'name' => 'event-rule-config-form', + 'id' => 'event-rule-config-form' + ]; + + /** @var ValidHtml[] */ + protected $options; + + /** @var array */ + protected $config; + + /** @var Url */ + protected $searchEditorUrl; + + /** + * 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; + } + + protected function assemble(): void + { + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + $this->add($this->createUidElement()); + + // Duplicate submit button for save + $this->addElement( + 'submitButton', + 'save', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + $this->addElement( + 'submitButton', + 'delete', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + $this->addElement( + 'submitButton', + 'discard_changes', + [ + 'hidden' => true, + 'class' => 'primary-submit-btn-duplicate' + ] + ); + + $this->addElement( + 'hidden', + 'zero-condition-escalation', + ['value' => 1] + ); + + /** @var string $objectFilter */ + $objectFilter = $this->config['object_filter'] ?? ''; + $configFilter = (new EventRuleConfigFilter('config-filter')) + ->setObjectFilter($objectFilter) + ->setSearchEditorUrl($this->searchEditorUrl); + $this->registerElement($configFilter); + + $addEscalationButton = $this->createElement( + 'submitButton', + 'add-escalation', + [ + 'class' => ['add-button', 'control-button', 'spinner'], + 'label' => new Icon('plus'), + 'title' => $this->translate('Add Escalation'), + 'formnovalidate' => true + ] + ); + + $escalationCountElement = $this->createElement( + 'hidden', + 'escalation-count', + ['value' => 1, 'class' => 'autosubmit'] + ); + + $this->registerElement($addEscalationButton); + $this->registerElement($escalationCountElement); + $removeEscalationButtons = []; + + $this->handleAdd(); + /** @var int $escalationCount */ + $escalationCount = $this->getValue('escalation-count'); + + for ($n = 1; $n <= $escalationCount; $n++) { + if ($escalationCount === 1) { + continue; + } + + $removeEscalationButtons[$n] = $this->createRemoveButton($n); + $this->registerElement($removeEscalationButtons[$n]); + } + + /** @var ?int $zeroConditionEscalation */ + $zeroConditionEscalation = $this->getValue('zero-condition-escalation'); + + if ($zeroConditionEscalation === null) { + $noZeroEscalationConditions = true; + } else { + $noZeroEscalationConditions = false; + } + + for ($i = 1; $i <= $this->getValue('escalation-count'); $i++) { + $escalationCondition = new EscalationCondition('escalation-condition_' . $i); + $escalationRecipient = new EscalationRecipient('escalation-recipient_' . $i); + + $escalation = new Escalation( + $escalationCondition, + $escalationRecipient, + $removeEscalationButtons[$i] ?? null + ); + + $this->registerElement($escalationCondition); + $this->registerElement($escalationRecipient); + + if ( + $this->getValue('zero-condition-escalation') === (string) $i + && $escalation->addConditionHasBeenPressed() + ) { + $noZeroEscalationConditions = true; + } elseif ( + $zeroConditionEscalation === null + && $escalation->singleConditionRemoved() + ) { + $noZeroEscalationConditions = false; + $this->getElement('zero-condition-escalation')->setValue($i); + } + + $this->escalations[$i] = $escalation; + } + + $this->handleRemove(); + + if ($noZeroEscalationConditions === false) { + $noZeroEscalationConditions = $this->getValue('zero-condition-escalation') === null; + } + + $this->add(Html::tag( + 'ul', + ['class' => 'filter-wrapper'], + [ + Html::tag('li', (new FlowLine())->getRightArrow()), + Html::tag('li', $configFilter), + Html::tag('li', (new FlowLine())->getHorizontalLine()) + ] + )); + + if ($noZeroEscalationConditions) { + foreach ($this->escalations as $escalation) { + $escalation->getCondition() + ->setAllowZeroConditions(true); + } + + $this->getElement('zero-condition-escalation') + ->setValue(null); + } else { + /** @var int $zeroConditionPosition */ + $zeroConditionPosition = $this->getValue('zero-condition-escalation'); + $this->escalations[$zeroConditionPosition] + ->getCondition() + ->setAllowZeroConditions(true); + } + + $this->add(new Escalations($this->escalations, $addEscalationButton)); + + $this->addElement($escalationCountElement); + } + + /** + * Handle addition of escalations + */ + protected function handleAdd(): void + { + $pressedButton = $this->getPressedSubmitElement(); + + if ($pressedButton && $pressedButton->getName() === 'add-escalation') { + $this->clearPopulatedValue('escalation-count'); + /** @var int $position */ + $position = $this->getValue('escalation-count') + 1; + $this->getElement('escalation-count') + ->setValue($position); + + if ($this->getValue('zero-condition-escalation') === null) { + $this->getElement('zero-condition-escalation') + ->setValue($position); + } + } + } + + /** + * Handle removal of escalations + */ + public function handleRemove(): void + { + $pressedButton = $this->getPressedSubmitElement(); + + if ( + $pressedButton + && strpos($pressedButton->getName(), 'remove-escalation') !== false + ) { + $buttonName = $pressedButton->getName(); + $this->clearPopulatedValue('escalation-count'); + /** @var int $position */ + $position = substr($buttonName, strpos($buttonName, "_") + 1); + unset($this->escalations[$position]); + $escalationCount = count($this->escalations); + if ($escalationCount === 1 && $position === 2) { + $this->escalations[1]->setHasNoRemovedButton(true); + } else { + for ($i = $position; $i <= $escalationCount; $i++) { + $nextCount = $i + 1; + $this->escalations[$nextCount]->condition->setName('escalation-condition_' . $i); + $this->escalations[$nextCount]->recipient->setName('escalation-recipient_' . $i); + if ($escalationCount === 1) { + $this->escalations[$nextCount]->setHasNoRemovedButton(true); + } elseif ($this->escalations[$nextCount]->removeButton) { + $this->escalations[$nextCount]->removeButton->setName('remove-escalation_' . $i); + } + } + } + + if ($this->getValue('zero-condition-escalation') === (string) $position) { + $this->getElement('zero-condition-escalation') + ->setValue(null); + } + + $this->getElement('escalation-count') + ->setValue($escalationCount); + } + } + + public function populate($values): self + { + if (! isset($values['rule_escalation'])) { + return parent::populate($values); + } + + $formValues = []; + $formValues['escalation-count'] = 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']; + foreach ($conditions as $key => $condition) { + if ($condition === '') { + if (! isset($formValues['zero-condition-escalation'])) { + $formValues['zero-condition-escalation'] = $position; + } + + $conditionFormValues['condition-count'] = 0; + continue; + } + + $count = $key + 1; + if (empty($condition)) { // when other conditions are removed and only 1 pending with no values + $conditionFormValues['column_' . $count] = null; + $conditionFormValues['operator_' . $count] = null; + $conditionFormValues['value_' . $count] = null; + + continue; + } + + /** @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_' . $position] = $conditionFormValues; + $recipientFormValues = []; + $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_' . $position] = $recipientFormValues; + } + + return parent::populate($formValues); + } + + /** + * Get the values for the given event rule config + * + * @return array values as name-value pairs + */ + public function getValues(): array + { + $values = []; + $escalations = []; + foreach ($this->getElements() as $element) { + if (! $element->isIgnored()) { + $name = explode('_', $element->getName()); + if ($name[0] === 'escalation-condition') { + /** @var EscalationCondition $element */ + $escalations[$name[1]]['condition-count'] = $element->getValue('condition-count'); + $escalations[$name[1]]['condition'] = $element->getCondition(); + $escalations[$name[1]]['id'] = $element->getValue('id'); + } elseif ($name[0] === 'escalation-recipient') { + /** @var EscalationRecipient $element */ + $escalations[$name[1]]['recipient-count'] = $element->getValue('recipient-count'); + $escalations[$name[1]]['recipients'] = $element->getRecipients(); + } elseif ($name[0] === 'config-filter') { + /** @var EventRuleConfigFilter $element */ + $values['object_filter'] = $element->getObjectFilter(); + } else { + $values[$element->getName()] = $element->getValue(); + } + } + } + + $values['rule_escalation'] = $escalations; + + return $values; + } + + /** + * Crete remove buttons for the given escalation position + * + * @param int $pos + * + * @return SubmitButtonElement + */ + protected function createRemoveButton(int $pos): SubmitButtonElement + { + /** @var array> $escalations */ + $escalations = $this->config['rule_escalation']; + $escalationId = $escalations[$pos]['id'] ?? null; + $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; + } + } + + /** @var SubmitButtonElement $button */ + $button = $this->createElement( + 'submitButton', + 'remove-escalation_' . $pos, + [ + 'class' => [ + 'remove-escalation', + 'remove-button', + 'control-button', + 'spinner' + ], + 'label' => new Icon('minus'), + 'formnovalidate' => true + ] + ); + + $button + ->getAttributes() + ->registerAttributeCallback('disabled', function () use ($disableRemoveButton) { + return $disableRemoveButton; + }) + ->registerAttributeCallback('title', function () use ($disableRemoveButton) { + if ($disableRemoveButton) { + return $this->translate( + 'There exist active incidents for this escalation and hence cannot be removed' + ); + } + + return $this->translate('Remove escalation'); + }); + + return $button; + } + + /** + * Edit an existing event rule + * + * @param string $id The id of the event rule + * @param array $config The new configuration + * + * @return void + */ + public function insertOrAddRule(string $id, array $config): void + { + $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)); + + /** @var array> $escalationsInCache */ + $escalationsInCache = $config['rule_escalation']; + + $escalationsToUpdate = []; + $escalationsToRemove = []; + + /** @var RuleEscalation $escalationInDB */ + foreach ($escalationsFromDb as $escalationInDB) { + $escalationId = $escalationInDB->id; + $escalationInCache = array_filter($escalationsInCache, function (array $element) use ($escalationId) { + return $element['id'] === (string) $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((int) $id, $escalationsToAdd, $db, true); + } + + if (! empty($escalationsToUpdate)) { + $this->insertOrUpdateEscalations((int) $id, $escalationsToUpdate, $db); + } + + $db->commitTransaction(); + } + + /** + * Insert to or update Escalations and its recipients in Db + * + * @param array> $escalations + * @param Connection $db + * @param bool $insert + * + * @return void + */ + private function insertOrUpdateEscalations( + int $ruleId, + array $escalations, + Connection $db, + bool $insert = false + ): void { + foreach ($escalations as $position => $escalationConfig) { + /** @var array> $recipientsFromConfig */ + $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 + ]); + + /** @var string $escalationId */ + $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) { + return $element['id'] === (string) $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]); + } + } + + /** @var array $recipientConfig */ + 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(); + } +} diff --git a/application/forms/EventRuleForm.php b/application/forms/EventRuleForm.php index 9ce497efc..ca286d1e7 100644 --- a/application/forms/EventRuleForm.php +++ b/application/forms/EventRuleForm.php @@ -14,7 +14,7 @@ class EventRuleForm extends CompatForm use CsrfCounterMeasure; use Translation; - protected function assemble() + protected function assemble(): void { $this->add($this->createCsrfCounterMeasure(Session::getSession()->getId())); diff --git a/git push -f b/git push -f new file mode 100644 index 000000000..752753875 --- /dev/null +++ b/git push -f @@ -0,0 +1,21 @@ +WIP + +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# +# Date: Wed Feb 28 18:08:33 2024 +0100 +# +# On branch event-rule-config-enhancement +# Your branch is up to date with 'origin/event-rule-config-enhancement'. +# +# Changes to be committed: +# modified: application/controllers/EventRuleController.php +# modified: application/controllers/EventRulesController.php +# modified: application/forms/EventRuleConfigElements/EscalationCondition.php +# modified: application/forms/EventRuleConfigElements/EscalationRecipient.php +# modified: application/forms/EventRuleConfigElements/EventRuleConfigFilter.php +# modified: application/forms/EventRuleConfigForm.php +# deleted: library/Notifications/Widget/Escalations.php +# new file: library/Notifications/Widget/ItemList/Escalation.php +# new file: library/Notifications/Widget/ItemList/Escalations.php +# diff --git a/library/Notifications/Model/Contact.php b/library/Notifications/Model/Contact.php index cbc3ac2df..47ee34e0d 100644 --- a/library/Notifications/Model/Contact.php +++ b/library/Notifications/Model/Contact.php @@ -9,6 +9,13 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +/** + * @property int $id + * @property string $full_name + * @property ?string $username + * @property string $color + * @property int $default_channel_id + */ class Contact extends Model { public function getTableName(): string diff --git a/library/Notifications/Model/Contactgroup.php b/library/Notifications/Model/Contactgroup.php index d7468b234..5f47bc906 100644 --- a/library/Notifications/Model/Contactgroup.php +++ b/library/Notifications/Model/Contactgroup.php @@ -7,6 +7,11 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +/** + * @property int $id + * @property string $name + * @property string $color + */ class Contactgroup extends Model { public function getTableName() diff --git a/library/Notifications/Model/RuleEscalation.php b/library/Notifications/Model/RuleEscalation.php index 78064eb67..54204efda 100644 --- a/library/Notifications/Model/RuleEscalation.php +++ b/library/Notifications/Model/RuleEscalation.php @@ -7,6 +7,14 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +/** + * @property int $id + * @property int $rule_id + * @property int $position + * @property ?string $condition + * @property ?string $name + * @property ?int $fallback_for + */ class RuleEscalation extends Model { public function getTableName() diff --git a/library/Notifications/Model/RuleEscalationRecipient.php b/library/Notifications/Model/RuleEscalationRecipient.php index a1f4bdcb3..b3d356206 100644 --- a/library/Notifications/Model/RuleEscalationRecipient.php +++ b/library/Notifications/Model/RuleEscalationRecipient.php @@ -7,6 +7,14 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +/** + * @property int $id + * @property int $rule_escalation_id + * @property ?int $contact_id + * @property ?int $contactgroup_id + * @property ?int $schedule_id + * @property ?int $channel_id + */ class RuleEscalationRecipient extends Model { public function getTableName() diff --git a/library/Notifications/Model/Schedule.php b/library/Notifications/Model/Schedule.php index def0997ef..d32a1db00 100644 --- a/library/Notifications/Model/Schedule.php +++ b/library/Notifications/Model/Schedule.php @@ -7,6 +7,10 @@ use ipl\Orm\Model; use ipl\Orm\Relations; +/** + * @property int $id + * @property string $name + */ class Schedule extends Model { public function getTableName() diff --git a/library/Notifications/Widget/Escalations.php b/library/Notifications/Widget/Escalations.php deleted file mode 100644 index de216093d..000000000 --- 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/ItemList/Escalation.php b/library/Notifications/Widget/ItemList/Escalation.php new file mode 100644 index 000000000..b1e57ffd2 --- /dev/null +++ b/library/Notifications/Widget/ItemList/Escalation.php @@ -0,0 +1,122 @@ + 'escalation']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button of the escalation widget */ + public $removeButton; + + /** @var EscalationCondition Escalation condition fieldset */ + public $condition; + + /** @var EscalationRecipient Escalation recipient fieldset */ + public $recipient; + + /** @var bool Whether the widget has a remove button */ + private $hasNoRemoveButton = false; + + public function __construct( + EscalationCondition $condition, + EscalationRecipient $recipient, + ?SubmitButtonElement $removeButton + ) { + $this->condition = $condition; + $this->recipient = $recipient; + $this->removeButton = $removeButton; + } + + /** + * Get escalation condition + * + * @return EscalationCondition + */ + public function getCondition(): EscalationCondition + { + return $this->condition; + } + + /** + * Check if the add button of the condition fieldset has been pressed + * + * @return bool + */ + public function addConditionHasBeenPressed(): bool + { + return $this->getCondition()->getPopulatedValue('add-condition') === 'y'; + } + + /** + * Check if the add button of the condition fieldset has been pressed + * + * @return bool + */ + public function singleConditionRemoved(): bool + { + return $this->getCondition()->getPopulatedValue('condition-count') === '1' + && $this->getCondition()->getPopulatedValue('remove_1') === 'y'; + } + + /** + * Get escalation recipient + * + * @return EscalationRecipient + */ + protected function getRecipient(): EscalationRecipient + { + return $this->recipient; + } + + /** + * Set if escalation has remove button + * + * @param bool $state + * + * @return $this + */ + public function setHasNoRemovedButton(bool $state): self + { + $this->hasNoRemoveButton = $state; + + return $this; + } + + /** + * Create first component of the escalation widget + * + * @return FlowLine|FormElement|null + */ + protected function createFirstComponent() + { + if ($this->hasNoRemoveButton || $this->removeButton === null) { + return (new FlowLine())->getHorizontalLine(); + } + + return $this->removeButton; + } + + protected function assemble(): void + { + $this->add([ + $this->createFirstComponent(), + (new FlowLine())->getRightArrow(), + $this->getCondition(), + (new FlowLine())->getRightArrow(), + $this->getRecipient() + ]); + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationConditionList.php b/library/Notifications/Widget/ItemList/EscalationConditionList.php new file mode 100644 index 000000000..3f9f9d6fe --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationConditionList.php @@ -0,0 +1,36 @@ + 'options']; + + protected $tag = 'ul'; + + /** @var EscalationConditionListItem[] Escalation conditions */ + private $conditions; + + /** + * Create EscalationConditionListItem for an escalation + * + * @param EscalationConditionListItem[] $conditions + */ + public function __construct(array $conditions) + { + $this->conditions = $conditions; + } + + protected function assemble(): void + { + $this->add([ + $this->conditions + ]); + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationConditionListItem.php b/library/Notifications/Widget/ItemList/EscalationConditionListItem.php new file mode 100644 index 000000000..0d288f226 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationConditionListItem.php @@ -0,0 +1,51 @@ + 'option']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button for the recipient */ + public $removeButton; + + /** @var BaseFormElement Condition type */ + public $conditionType; + + /** @var BaseFormElement Operator used for the condition */ + public $operator; + + /** @var BaseFormElement Condition value */ + public $conditionVal; + + public function __construct( + BaseFormElement $conditionType, + BaseFormElement $operator, + BaseFormElement $conditionVal, + ?SubmitButtonElement $removeButton + ) { + $this->conditionType = $conditionType; + $this->operator = $operator; + $this->conditionVal = $conditionVal; + $this->removeButton = $removeButton; + } + + protected function assemble(): void + { + $this->add([ + $this->conditionType, + $this->operator, + $this->conditionVal, + $this->removeButton + ]); + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationRecipientList.php b/library/Notifications/Widget/ItemList/EscalationRecipientList.php new file mode 100644 index 000000000..d3a233f7a --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationRecipientList.php @@ -0,0 +1,36 @@ + 'options']; + + protected $tag = 'ul'; + + /** @var EscalationRecipientListItem[] Escalation recipients */ + private $recipients; + + /** + * Create EscalationRecipientList for an escalation + * + * @param EscalationRecipientListItem[] $recipients + */ + public function __construct(array $recipients) + { + $this->recipients = $recipients; + } + + protected function assemble(): void + { + $this->add([ + $this->recipients + ]); + } +} diff --git a/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php b/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php new file mode 100644 index 000000000..201e04c32 --- /dev/null +++ b/library/Notifications/Widget/ItemList/EscalationRecipientListItem.php @@ -0,0 +1,45 @@ + 'option']; + + protected $tag = 'li'; + + /** @var ?SubmitButtonElement Remove button for the recipient */ + public $removeButton; + + /** @var BaseFormElement Recipient name field */ + public $recipient; + + /** @var BaseFormElement Recipient channel field */ + public $channel; + + public function __construct( + BaseFormElement $reipient, + BaseFormElement $channel, + ?SubmitButtonElement $removeButton + ) { + $this->recipient = $reipient; + $this->channel = $channel; + $this->removeButton = $removeButton; + } + + protected function assemble(): void + { + $this->add([ + $this->recipient, + $this->channel, + $this->removeButton + ]); + } +} diff --git a/library/Notifications/Widget/ItemList/Escalations.php b/library/Notifications/Widget/ItemList/Escalations.php new file mode 100644 index 000000000..fddbe369b --- /dev/null +++ b/library/Notifications/Widget/ItemList/Escalations.php @@ -0,0 +1,42 @@ + 'escalations']; + + protected $tag = 'ul'; + + /** @var Escalation[] */ + private $escalations; + + /** @var FormElement */ + private $addButton; + + /** + * Create Escalations for an event rule + * + * @param Escalation[] $escalations + * @param FormElement $addButton + */ + public function __construct(array $escalations, FormElement $addButton) + { + $this->escalations = $escalations; + $this->addButton = $addButton; + } + + protected function assemble(): void + { + $this->add([ + $this->escalations, + Html::tag('li', ['class' => 'add-escalation'], $this->addButton) + ]); + } +} diff --git a/public/css/detail/event-rule-detail.less b/public/css/detail/event-rule-detail.less index 6f85b48dd..24dbdf15c 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,65 @@ .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; -} + flex-direction: row-reverse; -#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; -} - -.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; -} + button[type="submit"]:not(:first-child) { + margin-right: 1em; -.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; - } - - input[type='text'] { - max-width: unset; - width: 25em; - } + &:disabled { + background: @gray-light; + color: @disabled-gray; + cursor: not-allowed; + border: transparent; } - } - .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; + &.btn-remove { + border: none; + .button(@body-bg-color, @color-critical, @color-critical-accentuated); &:disabled { - background: @gray-light; + background: none; 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(); + cursor: not-allowed; } } - } -} -.remove-escalation-form { - button[disabled] { - &:disabled { - background: @gray-light; - color: @disabled-gray; - cursor: not-allowed; + &.btn-discard-changes { + .event-rule-button(); } } } diff --git a/public/css/event-rule-config.less b/public/css/event-rule-config.less new file mode 100644 index 000000000..064f1b6ef --- /dev/null +++ b/public/css/event-rule-config.less @@ -0,0 +1,323 @@ +.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 { + margin: 0; + padding: 0; + height: fit-content; + display: inline-flex; + align-self: flex-start; + } + + .filter-wrapper:has(.config-filter .search-controls) { + margin-top: .55em; + } + + .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; + } + + .horizontal-line, + .right-arrow { + margin-top: 2em; + align-self: flex-start; + } + + .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; + } + + .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; + } + } +} + +.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: 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; + } + + input::-webkit-calendar-picker-indicator { + display: none; + } +} + +datalist { + +} + +.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, +.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(); + } +} + +.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; +}