Skip to content

Commit

Permalink
ContactForm: Show suggestions while typing in user element
Browse files Browse the repository at this point in the history
Introduce IcingaWebUserSuggestions class
ContactControler: Add suggestion action and remove dead code
  • Loading branch information
sukhwinder33445 committed Feb 21, 2025
1 parent 4812a81 commit 5795311
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 37 deletions.
13 changes: 9 additions & 4 deletions application/controllers/ContactController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
namespace Icinga\Module\Notifications\Controllers;

use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Model\Contact;
use Icinga\Module\Notifications\Web\Form\ContactForm;
use Icinga\Module\Notifications\Widget\IcingaWebUserSuggestions;
use Icinga\Web\Notification;
use ipl\Html\FormElement\FieldsetElement;
use ipl\Sql\Connection;
use ipl\Stdlib\Filter;
use ipl\Web\Compat\CompatController;

class ContactController extends CompatController
Expand Down Expand Up @@ -48,4 +45,12 @@ public function indexAction(): void

$this->addContent($form);
}

public function suggestIcingaWebUserAction(): void
{
$users = new IcingaWebUserSuggestions();
$users->forRequest($this->getServerRequest());

$this->getDocument()->addHtml($users);
}
}
79 changes: 46 additions & 33 deletions library/Notifications/Web/Form/ContactForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ipl\Validator\StringLengthValidator;
use ipl\Web\Common\CsrfCounterMeasure;
use ipl\Web\Compat\CompatForm;
use ipl\Web\Url;

class ContactForm extends CompatForm
{
Expand Down Expand Up @@ -93,40 +94,52 @@ protected function assemble()
'label' => $this->translate('Contact Name'),
'required' => true
]
)->addElement(
'text',
'username',
[
'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;
})
);

$suggestionsId = 'icinga-user-suggestions';
$contact
->addHtml(new HtmlElement('div', new Attributes(['id' => $suggestionsId, 'class' => 'search-suggestions'])))
->addElement(
'text',
'username',
[
'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;
})
],
'placeholder' => $this->translate('Start typing to see suggestions ...'),
'autocomplete' => 'off',
'class' => 'search',
'data-enrichment-type' => 'completion',
'data-term-suggestions' => '#' . $suggestionsId,
'data-suggest-url' => Url::fromPath('notifications/contact/suggest-icinga-web-user')
->with(['showCompact' => true, '_disableLayout' => 1]),
]
]
)->addHtml(new HtmlElement(
'p',
new Attributes(['class' => 'description']),
new Text($this->translate(
"Link existing Icinga Web users. Users from external authentication backends"
. " won't be suggested and must be entered manually."
))
));
)->addHtml(new HtmlElement(
'p',
new Attributes(['class' => 'description']),
new Text($this->translate(
"Link existing Icinga Web users. Users from external authentication backends"
. " won't be suggested and must be entered manually."
))
));

$defaultChannel = $this->createElement(
'select',
Expand Down
158 changes: 158 additions & 0 deletions library/Notifications/Widget/IcingaWebUserSuggestions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Widget;

use Exception;
use Icinga\Application\Config;
use Icinga\Authentication\User\DomainAwareInterface;
use Icinga\Authentication\User\UserBackend;
use Icinga\Data\Selectable;
use Icinga\Repository\Repository;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Web\Control\SearchBar\Suggestions;
use Psr\Http\Message\ServerRequestInterface;

class IcingaWebUserSuggestions extends BaseHtmlElement
{
use Translation;

protected $tag = 'ul';

/** @var string */
protected $searchTerm;

/** @var string */
protected $originalValue;

public function setSearchTerm(string $term): self
{
$this->searchTerm = $term;

return $this;
}

public function setOriginalValue(string $term): self
{
$this->originalValue = $term;

return $this;
}

/**
* Load suggestions as requested by the client
*
* @param ServerRequestInterface $request
*
* @return $this
*/
public function forRequest(ServerRequestInterface $request): self
{
if ($request->getMethod() !== 'POST') {
return $this;
}

$requestData = json_decode($request->getBody()->read(8192), true);
if (empty($requestData)) {
return $this;
}

$this->setSearchTerm($requestData['term']['label']);
$this->setOriginalValue($requestData['term']['search']);

return $this;
}

protected function assemble(): void
{
$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', $this->searchTerm)
->limit($limit);

try {
$names = $query->fetchColumn();
} catch (Exception $e) {
continue;
}

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

if ($backend instanceof DomainAwareInterface) {
$names = array_map(function ($name) use ($backend) {
return $name . '@' . $backend->getDomain();
}, $names);
}

$this->addHtml(
new HtmlElement(
'li',
new Attributes(['class' => Suggestions::SUGGESTION_TITLE_CLASS]),
new Text($this->translate('Backend')),
new HtmlElement('span', new Attributes(['class' => 'badge']), new Text($backend->getName()))
)
);

foreach ($names as $name) {
$this->addHtml(
new HtmlElement(
'li',
null,
new HtmlElement(
'input',
Attributes::create([
'type' => 'button',
'value' => $name,
'data-label' => $name,
'data-search' => $name,
'data-class' => 'icinga-web-user',
])
)
)
);
}

$limit -= count($names);
}

if ($this->isEmpty()) {
$this->addHtml(
new HtmlElement(
'li',
Attributes::create(['class' => 'nothing-to-suggest']),
new HtmlElement('em', null, Text::create($this->translate('Nothing to suggest')))
)
);
}
}

public function renderUnwrapped(): string
{
$this->ensureAssembled();

if ($this->isEmpty()) {
return '';
}

return parent::renderUnwrapped();
}
}
8 changes: 8 additions & 0 deletions public/css/form.less
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,12 @@
padding-bottom: 1em;
border-bottom: 1px solid @gray-light;
}

input.search {
padding-left: 1.5em; // property was overwritten .icinga-controls
}

.search-suggestions .badge {
margin-left: 0.5em;
}
}

0 comments on commit 5795311

Please sign in to comment.