From 19b5cc85d5b504fd45b205e1f481a558e8f66b9d Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Mon, 23 Oct 2023 12:09:46 +0200 Subject: [PATCH] SearchBar: Make tag/extra_tag searchable --- .../Model/Behavior/ObjectTags.php | 79 +++++++++++++++++ library/Notifications/Model/Event.php | 2 +- library/Notifications/Model/ExtraTag.php | 28 ++++++ library/Notifications/Model/Incident.php | 2 +- library/Notifications/Model/Tag.php | 28 ++++++ .../Control/SearchBar/ObjectSuggestions.php | 85 +++++++++++++++++++ 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 library/Notifications/Model/Behavior/ObjectTags.php create mode 100644 library/Notifications/Model/ExtraTag.php create mode 100644 library/Notifications/Model/Tag.php diff --git a/library/Notifications/Model/Behavior/ObjectTags.php b/library/Notifications/Model/Behavior/ObjectTags.php new file mode 100644 index 000000000..39eaeac52 --- /dev/null +++ b/library/Notifications/Model/Behavior/ObjectTags.php @@ -0,0 +1,79 @@ +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', '..'), $name)); + } + + public function isSelectableColumn(string $name): bool + { + return true; + } +} diff --git a/library/Notifications/Model/Event.php b/library/Notifications/Model/Event.php index a75c5a555..573d9564a 100644 --- a/library/Notifications/Model/Event.php +++ b/library/Notifications/Model/Event.php @@ -48,7 +48,7 @@ public function getColumnDefinitions() public function getSearchColumns() { - return ['object.host', 'object.service']; + return []; } public function getDefaultSort() diff --git a/library/Notifications/Model/ExtraTag.php b/library/Notifications/Model/ExtraTag.php new file mode 100644 index 000000000..8ded4008e --- /dev/null +++ b/library/Notifications/Model/ExtraTag.php @@ -0,0 +1,28 @@ +add(new ObjectTags()); + } +} diff --git a/library/Notifications/Model/Incident.php b/library/Notifications/Model/Incident.php index 6fb0136bf..172f0a85f 100644 --- a/library/Notifications/Model/Incident.php +++ b/library/Notifications/Model/Incident.php @@ -44,7 +44,7 @@ public function getColumnDefinitions() public function getSearchColumns() { - return ['object.host', 'object.service']; + return []; } public function getDefaultSort() diff --git a/library/Notifications/Model/Tag.php b/library/Notifications/Model/Tag.php new file mode 100644 index 000000000..818e63615 --- /dev/null +++ b/library/Notifications/Model/Tag.php @@ -0,0 +1,28 @@ +add(new ObjectTags()); + } +} diff --git a/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php b/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php index 97cf8f311..2cd73d018 100644 --- a/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php +++ b/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php @@ -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; @@ -20,6 +25,8 @@ class ObjectSuggestions extends Suggestions { + use Auth; + /** @var Model */ protected $model; @@ -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); @@ -84,6 +95,9 @@ protected function createQuickSearchFilter($searchTerm) return $quickFilter; } + /** + * @throws SearchException + */ protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $searchFilter) { $model = $this->getModel(); @@ -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); @@ -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())) @@ -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', '..') : t('Object Extra %s', '..'); + $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)) {