Skip to content

Commit

Permalink
SearchBar: Make tag/extra_tag searchable
Browse files Browse the repository at this point in the history
  • Loading branch information
sukhwinder33445 committed Oct 23, 2023
1 parent b560154 commit 19b5cc8
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 2 deletions.
79 changes: 79 additions & 0 deletions library/Notifications/Model/Behavior/ObjectTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Icinga\Module\Notifications\Model\Behavior;

use Icinga\Module\Notifications\Common\Auth;
use ipl\Orm\AliasedExpression;
use ipl\Orm\ColumnDefinition;
use ipl\Orm\Contract\QueryAwareBehavior;
use ipl\Orm\Contract\RewriteColumnBehavior;
use ipl\Orm\Query;
use ipl\Stdlib\Filter;
use ipl\Stdlib\Filter\Rule;

class ObjectTags implements RewriteColumnBehavior, QueryAwareBehavior
{
use Auth;

/** @var Query */
protected $query;

public function setQuery(Query $query): self
{
$this->query = $query;

return $this;
}

public function rewriteCondition(Filter\Condition $condition, $relation = null): ?Rule
{
$filterAll = null;
$column = $condition->metaData()->get('columnName');
if ($column !== null) {
if (substr($relation, -4) === 'tag.') {
$relation = substr($relation, 0, -4) . 'object_id_tag.';
} else { // extra_tag.
$relation = substr($relation, 0, -10) . 'object_extra_tag.';
}

$nameFilter = Filter::like($relation . 'tag', $column);
$class = get_class($condition);
$valueFilter = new $class($relation . 'value', $condition->getValue());

$filterAll = Filter::all($nameFilter, $valueFilter);
}

return $filterAll;
}

public function rewriteColumn($column, $relation = null): AliasedExpression
{
$model = $this->query->getModel();
$subQuery = $this->query->createSubQuery(new $model(), $relation)
->limit(1)
->columns('value')
->filter(Filter::equal('tag', $column));

$this->applyRestrictions($subQuery);

$alias = $this->query->getDb()->quoteIdentifier([str_replace('.', '_', $relation) . "_$column"]);

[$select, $values] = $this->query->getDb()->getQueryBuilder()->assembleSelect($subQuery->assembleSelect());
return new AliasedExpression($alias, "($select)", null, ...$values);
}

public function rewriteColumnDefinition(ColumnDefinition $def, string $relation): void
{
$parts = explode('.', substr($relation, 0, -4));
$objectType = array_pop($parts);

$name = $def->getName();
// Programmatically translated since the full definition is available in class ObjectSuggestions
$def->setLabel(sprintf(t(ucfirst($objectType) . ' %s', '..<tag-name>'), $name));
}

public function isSelectableColumn(string $name): bool
{
return true;
}
}
2 changes: 1 addition & 1 deletion library/Notifications/Model/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function getColumnDefinitions()

public function getSearchColumns()
{
return ['object.host', 'object.service'];
return [];
}

public function getDefaultSort()
Expand Down
28 changes: 28 additions & 0 deletions library/Notifications/Model/ExtraTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

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

namespace Icinga\Module\Notifications\Model;

use Icinga\Module\Notifications\Model\Behavior\ObjectTags;
use ipl\Orm\Behaviors;
use ipl\Sql\Connection;

class ExtraTag extends ObjectExtraTag
{
/**
* @internal Don't use. This model acts only as relation target and is not supposed to be directly used as query
* target. Use {@see CustomvarFlat} instead.
*/
public static function on(Connection $_)
{
throw new \LogicException('Documentation says: DO NOT USE. Can\'t you read?');
}

public function createBehaviors(Behaviors $behaviors): void
{
parent::createBehaviors($behaviors);

$behaviors->add(new ObjectTags());
}
}
2 changes: 1 addition & 1 deletion library/Notifications/Model/Incident.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function getColumnDefinitions()

public function getSearchColumns()
{
return ['object.host', 'object.service'];
return [];
}

public function getDefaultSort()
Expand Down
28 changes: 28 additions & 0 deletions library/Notifications/Model/Tag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

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

namespace Icinga\Module\Notifications\Model;

use Icinga\Module\Notifications\Model\Behavior\ObjectTags;
use ipl\Orm\Behaviors;
use ipl\Sql\Connection;

class Tag extends ObjectIdTag
{
/**
* @internal Don't use. This model acts only as relation target and is not supposed to be directly used as query
* target. Use {@see CustomvarFlat} instead.
*/
public static function on(Connection $_)
{
throw new \LogicException('Documentation says: DO NOT USE. Can\'t you read?');
}

public function createBehaviors(Behaviors $behaviors): void
{
parent::createBehaviors($behaviors);

$behaviors->add(new ObjectTags());
}
}
85 changes: 85 additions & 0 deletions library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace Icinga\Module\Notifications\Web\Control\SearchBar;

use Icinga\Module\Notifications\Common\Auth;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Model\ObjectExtraTag;
use Icinga\Module\Notifications\Model\ObjectIdTag;
use Icinga\Module\Notifications\Util\ObjectSuggestionsCursor;
use ipl\Html\HtmlElement;
use ipl\Orm\Exception\InvalidColumnException;
use ipl\Orm\Model;
use ipl\Orm\Query;
use ipl\Orm\Relation;
use ipl\Orm\Relation\HasOne;
use ipl\Orm\Resolver;
Expand All @@ -20,6 +25,8 @@

class ObjectSuggestions extends Suggestions
{
use Auth;

/** @var Model */
protected $model;

Expand Down Expand Up @@ -59,6 +66,10 @@ public function getModel(): Model

protected function shouldShowRelationFor(string $column): bool
{
if (strpos($column, '.tag.') !== false || strpos($column, '.extra_tag.') !== false) {
return false;
}

$tableName = $this->getModel()->getTableName();
$columnPath = explode('.', $column);

Expand All @@ -84,6 +95,9 @@ protected function createQuickSearchFilter($searchTerm)
return $quickFilter;
}

/**
* @throws SearchException
*/
protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $searchFilter)
{
$model = $this->getModel();
Expand All @@ -104,6 +118,29 @@ protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $sea
}

$columnPath = $query->getResolver()->qualifyPath($column, $model->getTableName());
list($targetPath, $columnName) = preg_split('/(?<=tag|extra_tag)\.|\.(?=[^.]+$)/', $columnPath, 2);

$isTag = false;
if (substr($targetPath, -4) === '.tag') {
$isTag = true;
$targetPath = substr($targetPath, 0, -3) . 'object_id_tag';
} elseif (substr($targetPath, -10) === '.extra_tag') {
$isTag = true;
$targetPath = substr($targetPath, 0, -9) . 'object_extra_tag';
}

if (strpos($targetPath, '.') !== false) {
try {
$query->with($targetPath); // TODO: Remove this, once ipl/orm does it as early
} catch (InvalidRelationException $e) {
throw new SearchException(sprintf(t('"%s" is not a valid relation'), $e->getRelation()));
}
}

if ($isTag) {
$columnPath = $targetPath . '.value';
$query->filter(Filter::like($targetPath . '.tag', $columnName));
}

$inputFilter = Filter::like($columnPath, $searchTerm);
$query->columns($columnPath);
Expand All @@ -125,6 +162,7 @@ protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $sea
}

$query->filter($searchFilter);
$this->applyRestrictions($query);

try {
return (new ObjectSuggestionsCursor($query->getDb(), $query->assembleSelect()->distinct()))
Expand All @@ -143,8 +181,55 @@ protected function fetchColumnSuggestions($searchTerm)
foreach (self::collectFilterColumns($model, $query->getResolver()) as $columnName => $columnMeta) {
yield $columnName => $columnMeta;
}

// Custom variables only after the columns are exhausted and there's actually a chance the user sees them
foreach ([new ObjectIdTag(), new ObjectExtraTag()] as $model) {
$titleAdded = false;
foreach ($this->queryTags($model, $searchTerm) as $tag) {
$isIdTag = $tag instanceof ObjectIdTag;

if (! $titleAdded) {
$titleAdded = true;
$this->addHtml(HtmlElement::create(
'li',
['class' => static::SUGGESTION_TITLE_CLASS],
$isIdTag ? t('Object Tags') : t('Object Extra Tags')
));
}

$relation = $isIdTag ? 'object.tag' : 'object.extra_tag';
$label = $isIdTag ? t('Object %s', '..<object-id-tag>') : t('Object Extra %s', '..<object-extra-tag>');
$search = $name = $tag->tag;

yield $relation . '.' . $search => sprintf($label, $name);
}
}
}

/**
* Prepare query with all available tags/extra_tags from provided model matching the given term
*
* @param Model $model The model to fetch tag/extra_tag from
* @param string $searchTerm The given search term
*
* @return Query
*/
protected function queryTags(Model $model, string $searchTerm): Query
{
$tags = $model::on(Database::get())
->columns('tag')
->filter(Filter::like('tag', $searchTerm));
$this->applyRestrictions($tags);

$resolver = $tags->getResolver();
$tagColumn = $resolver->qualifyColumn('tag', $resolver->getAlias($tags->getModel()));

$tags->getSelectBase()->groupBy($tagColumn)->limit(static::DEFAULT_LIMIT);

return $tags;
}


protected function matchSuggestion($path, $label, $searchTerm)
{
if (preg_match('/[_.](id)$/', $path)) {
Expand Down

0 comments on commit 19b5cc8

Please sign in to comment.