Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/controllers/ChannelsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
$form->getValue('name')
)
);
$this->redirectNow(Links::channels());
$this->switchToSingleColumnLayout();
})
->handleRequest($this->getServerRequest());

Expand Down Expand Up @@ -166,7 +166,7 @@
{
/** @var Tab $tab */
foreach ($tabs->getTabs() as $tab) {
$this->tabs->add($tab->getName(), $tab);

Check failure on line 169 in application/controllers/ChannelsController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #1 $name of method ipl\Web\Widget\Tabs::add() expects string, string|null given.

Check failure on line 169 in application/controllers/ChannelsController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #1 $name of method ipl\Web\Widget\Tabs::add() expects string, string|null given.

Check failure on line 169 in application/controllers/ChannelsController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #1 $name of method ipl\Web\Widget\Tabs::add() expects string, string|null given.
}
}
}
63 changes: 63 additions & 0 deletions application/controllers/ContactController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

namespace Icinga\Module\Notifications\Controllers;

use Exception;
use Icinga\Application\Config;
use Icinga\Authentication\User\DomainAwareInterface;
use Icinga\Authentication\User\UserBackend;
use Icinga\Data\Selectable;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Web\Form\ContactForm;
use Icinga\Repository\Repository;
use Icinga\Web\Notification;
use ipl\Web\Compat\CompatController;
use ipl\Web\FormElement\SearchSuggestions;

class ContactController extends CompatController
{
Expand All @@ -21,7 +28,7 @@
$contactId = $this->params->getRequired('id');

$form = (new ContactForm(Database::get()))
->loadContact($contactId)

Check failure on line 31 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #1 $id of method Icinga\Module\Notifications\Web\Form\ContactForm::loadContact() expects int, mixed given.

Check failure on line 31 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #1 $id of method Icinga\Module\Notifications\Web\Form\ContactForm::loadContact() expects int, mixed given.

Check failure on line 31 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #1 $id of method Icinga\Module\Notifications\Web\Form\ContactForm::loadContact() expects int, mixed given.
->on(ContactForm::ON_SUCCESS, function (ContactForm $form) {
$form->editContact();
Notification::success(sprintf(
Expand All @@ -44,4 +51,60 @@

$this->addContent($form);
}

public function suggestIcingaWebUserAction(): void
{
$suggestions = new SearchSuggestions((function () use (&$suggestions) {
$userBackends = [];
foreach (Config::app('authentication') as $backendName => $backendConfig) {
$candidate = UserBackend::create($backendName, $backendConfig);
if ($candidate instanceof Selectable) {
$userBackends[] = $candidate;
}
}

$limit = 10;
while ($limit > 0 && ! empty($userBackends)) {
/** @var Repository $backend */
$backend = array_shift($userBackends);
$query = $backend->select()
->from('user', ['user_name'])
->where('user_name', $suggestions->getSearchTerm())

Check failure on line 72 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Cannot call method getSearchTerm() on ipl\Web\FormElement\SearchSuggestions|null.

Check failure on line 72 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Cannot call method getSearchTerm() on ipl\Web\FormElement\SearchSuggestions|null.

Check failure on line 72 in application/controllers/ContactController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Cannot call method getSearchTerm() on ipl\Web\FormElement\SearchSuggestions|null.
->limit($limit);

try {
/** @var string[] $names */
$names = $query->fetchColumn();
} catch (Exception) {
continue;
}

if (empty($names)) {
continue;
}

$domain = null;
if ($backend instanceof DomainAwareInterface && $backend->getDomain()) {
$domain = '@' . $backend->getDomain();
}

foreach ($names as $name) {
yield [
'search' => $name . $domain,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phpstan:

Binary operation "." between mixed and non-falsy-string|null results in an error.

'label' => $name . $domain,
'backend' => $backend->getName(),
];
}

$limit -= count($names);
}
})());

$suggestions->setGroupingCallback(function (array $data) {
return $data['backend'];
});

$suggestions->forRequest($this->getServerRequest());
$this->getDocument()->addHtml($suggestions);
}
}
35 changes: 30 additions & 5 deletions application/controllers/ContactsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@
namespace Icinga\Module\Notifications\Controllers;

use Icinga\Module\Notifications\Common\Links;
use Icinga\Module\Notifications\Model\Channel;
use Icinga\Module\Notifications\View\ContactRenderer;
use Icinga\Module\Notifications\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Model\Contact;
use Icinga\Module\Notifications\Web\Form\ContactForm;
use Icinga\Module\Notifications\Widget\ItemList\ObjectList;
use Icinga\Web\Notification;
use ipl\Html\HtmlElement;
use ipl\Html\TemplateString;
use ipl\Sql\Connection;
use ipl\Sql\Expression;
use ipl\Stdlib\Filter;
use ipl\Web\Compat\CompatController;
use ipl\Web\Compat\SearchControls;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
use ipl\Web\Filter\QueryString;
use ipl\Web\Layout\MinimalItemLayout;
use ipl\Web\Widget\ActionLink;
use ipl\Web\Widget\ButtonLink;

class ContactsController extends CompatController
Expand Down Expand Up @@ -79,15 +84,35 @@ public function indexAction()
$this->addControl($sortControl);
$this->addControl($limitControl);
$this->addControl($searchBar);
$this->addContent(
(new ButtonLink(t('Add Contact'), Links::contactAdd(), 'plus'))
->setBaseTarget('_next')
->addAttributes(['class' => 'add-new-component'])
);

$addButton = (new ButtonLink(
t('Add Contact'),
Links::contactAdd(),
'plus',
['class' => 'add-new-component']
))->setBaseTarget('_next');

$emptyStateMessage = null;
if (Channel::on($this->db)->columns([new Expression('1')])->first() === null) {
$addButton->disable(t('A channel is required to add a contact'));

$emptyStateMessage = new HtmlElement(
'span',
content: TemplateString::create(
$this->translate(
'No contacts found. To add a new contact, please {{#link}}configure a Channel{{/link}} first.'
),
['link' => new ActionLink(null, Links::channelAdd(), attributes: ['data-base-target' => '_next'])]
)
);
}

$this->addContent($addButton);

$this->addContent(
(new ObjectList($contacts, new ContactRenderer()))
->setItemLayoutClass(MinimalItemLayout::class)
->setEmptyStateMessage($emptyStateMessage)
);

if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
Expand Down
144 changes: 102 additions & 42 deletions library/Notifications/Web/Form/ContactForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@
use Icinga\Module\Notifications\Model\RotationMember;
use Icinga\Module\Notifications\Model\RuleEscalationRecipient;
use Icinga\Web\Session;
use ipl\Html\Attributes;
use ipl\Html\Contract\FormSubmitElement;
use ipl\Html\FormElement\FieldsetElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Sql\Connection;
use ipl\Stdlib\Filter;
use ipl\Validator\CallbackValidator;
use ipl\Validator\EmailAddressValidator;
use ipl\Validator\StringLengthValidator;
use ipl\Web\Common\CsrfCounterMeasure;
use ipl\Web\Compat\CompatForm;
use ipl\Web\FormElement\SuggestionElement;
use ipl\Web\Url;

class ContactForm extends CompatForm
{
Expand Down Expand Up @@ -71,7 +76,8 @@ public function isValidEvent($event)

protected function assemble()
{
$this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
$this->addAttributes(['class' => 'contact-form']);
$this->addCsrfCounterMeasure(Session::getSession()->getId());

// Fieldset for contact full name and username
$contact = (new FieldsetElement(
Expand All @@ -83,61 +89,106 @@ protected function assemble()

$this->addElement($contact);

$channelOptions = ['' => sprintf(' - %s - ', $this->translate('Please choose'))];
$channelOptions += Channel::fetchChannelNames($this->db);

$contact->addElement(
'text',
'full_name',
[
'label' => $this->translate('Full Name'),
'label' => $this->translate('Contact Name'),
'required' => true
]
)->addElement(
'text',
'username',
[
'label' => $this->translate('Username'),
'validators' => [
new StringLengthValidator(['max' => 254]),
new CallbackValidator(function ($value, $validator) {
$contact = Contact::on($this->db)
->filter(Filter::equal('username', $value));
if ($this->contactId) {
$contact->filter(Filter::unequal('id', $this->contactId));
}

if ($contact->first() !== null) {
$validator->addMessage($this->translate(
'A contact with the same username already exists.'
));

return false;
}

return true;
})
]
]
)->addElement(
);

$contact
->addElement(
new SuggestionElement(
'username',
Url::fromPath(
'notifications/contact/suggest-icinga-web-user',
['showCompact' => true, '_disableLayout' => 1]
),
[
'label' => $this->translate('Icinga Web User'),
'validators' => [
new StringLengthValidator(['max' => 254]),
new CallbackValidator(function ($value, $validator) {
$contact = Contact::on($this->db)
->filter(Filter::equal('username', $value));
if ($this->contactId) {
$contact->filter(Filter::unequal('id', $this->contactId));
}

if ($contact->first() !== null) {
$validator->addMessage($this->translate(
'A contact with the same username already exists.'
));

return false;
}

return true;
})
]
]
)
)
->addHtml(
new HtmlElement(
'p',
new Attributes(['class' => 'description']),
new Text($this->translate(
'Add an Icinga Web user to associate with this contact. Users from external authentication'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, but Add an Icinga Web user to associate with this contact. and Link existing Icinga Web users. both describe the same outcome.

Use this:

Use this to associate actions in the UI, such as incident management, with this contact. To successfully receive desktop notifications, this is also required.

. " backends won't be suggested and must be entered manually."
))
)
);

$channelQuery = Channel::on($this->db)
->columns(['id', 'name', 'type']);

$channelNames = [];
$channelTypes = [];
foreach ($channelQuery as $channel) {
$channelNames[$channel->id] = $channel->name;
$channelTypes[$channel->id] = $channel->type;
}

$defaultChannel = $this->createElement(
'select',
'default_channel_id',
[
'label' => $this->translate('Default Channel'),
'required' => true,
'class' => 'autosubmit',
'disabledOptions' => [''],
'options' => $channelOptions
'options' => [
'' => sprintf(' - %s - ', $this->translate('Please choose'))
] + $channelNames,
]
);

$this->addAddressElements();
$contact->registerElement($defaultChannel);

$this->addAddressElements($channelTypes[$defaultChannel->getValue()] ?? null);

$this->addHtml(new HtmlElement('hr'));

$this->decorate($defaultChannel);
$this->addHtml($defaultChannel);
$this->addHtml(new HtmlElement(
'p',
new Attributes(['class' => 'description']),
new Text($this->translate(
'Use this to associate actions in the UI, such as incident management, with this contact.'
. ' To successfully receive desktop notifications, this is also required.'
))
));

$this->addElement(
'submit',
'submit',
[
'label' => $this->contactId === null ?
$this->translate('Add Contact') :
$this->translate('Create Contact') :
$this->translate('Save Changes')
]
);
Expand All @@ -147,7 +198,7 @@ protected function assemble()
'submit',
'delete',
[
'label' => $this->translate('Delete'),
'label' => $this->translate('Delete Contact'),
'class' => 'btn-remove',
'formnovalidate' => true
]
Expand Down Expand Up @@ -380,7 +431,7 @@ private function fetchDbValues(): array
throw new HttpNotFoundException(t('Contact not found'));
}

$values['contact'] = [
$values['contact'] = [
'full_name' => $contact->full_name,
'username' => $contact->username,
'default_channel_id' => (string) $contact->default_channel_id
Expand All @@ -399,9 +450,11 @@ private function fetchDbValues(): array
/**
* Add address elements for all existing channel plugins
*
* @param ?string $defaultType The selected default channel type
*
* @return void
*/
private function addAddressElements(): void
private function addAddressElements(?string $defaultType): void
{
$plugins = $this->db->fetchPairs(
AvailableChannelType::on($this->db)
Expand All @@ -413,13 +466,20 @@ private function addAddressElements(): void
return;
}

$address = new FieldsetElement('contact_address', ['label' => $this->translate('Addresses')]);
$address = new FieldsetElement('contact_address', ['label' => $this->translate('Channels')]);
$address->addHtml(new HtmlElement(
'p',
new Attributes(['class' => 'description']),
new Text($this->translate('Configure the channels available for this contact here.'))
));

$this->addElement($address);

foreach ($plugins as $type => $label) {
foreach ($plugins as $type => $name) {
$element = $this->createElement('text', $type, [
'label' => $label,
'validators' => [new StringLengthValidator(['max' => 255])]
'label' => $name,
'validators' => [new StringLengthValidator(['max' => 255])],
'required' => $type === $defaultType
]);

if ($type === 'email') {
Expand Down
Loading
Loading