Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
Metabase
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package Registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Engineering Digital Service
Metabase
Commits
be0d5b90
Unverified
Commit
be0d5b90
authored
5 years ago
by
Cam Saul
Committed by
GitHub
5 years ago
Browse files
Options
Downloads
Patches
Plain Diff
Include Table info in Metric/Segment search results (#10305)
parent
3fddc4cd
No related branches found
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
src/metabase/api/search.clj
+231
-143
231 additions, 143 deletions
src/metabase/api/search.clj
test/metabase/api/search_test.clj
+87
-52
87 additions, 52 deletions
test/metabase/api/search_test.clj
with
318 additions
and
195 deletions
src/metabase/api/search.clj
+
231
−
143
View file @
be0d5b90
(
ns
metabase.api.search
(
:require
[
clojure.string
:as
str
]
[
compojure.core
:refer
[
GET
]]
[
honeysql.helpers
:as
h
]
[
metabase.api.common
:refer
[
*current-user-id*
*current-user-permissions-set*
check-403
defendpoint
define-routes
]]
[
flatland.ordered.map
:as
ordered-map
]
[
honeysql
[
core
:as
hsql
]
[
helpers
:as
h
]]
[
metabase
[
db
:as
mdb
]
[
util
:as
u
]]
[
metabase.api.common
:as
api
]
[
metabase.models
[
card
:refer
[
Card
]]
[
card-favorite
:refer
[
CardFavorite
]]
...
...
@@ -12,202 +17,285 @@
[
dashboard-favorite
:refer
[
DashboardFavorite
]]
[
metric
:refer
[
Metric
]]
[
pulse
:refer
[
Pulse
]]
[
segment
:refer
[
Segment
]]
]
[
metabase.util
:as
u
]
[
segment
:refer
[
Segment
]]
[
table
:refer
[
Table
]]
]
[
metabase.util
[
honeysql-extensions
:as
hx
]
[
schema
:as
su
]]
[
schema.core
:as
s
]
[
toucan.db
:as
db
]))
(
def
^
:private
SearchContext
"Map with the various allowed search parameters, used to construct the SQL query"
{
:search-string
(
s/maybe
su/NonBlankString
)
:archived?
s/Bool
:visible-collections
coll/VisibleCollections
})
(
def
^
:private
searchable-models
[
Card
Dashboard
Pulse
Collection
Segment
Metric
])
(
def
^
:private
SearchableModel
(
apply
s/enum
searchable-models
))
(
def
^
:private
HoneySQLColumn
(
s/cond-pre
s/Keyword
[(
s/one
s/Any
"column or value"
)
(
s/one
s/Keyword
"alias"
)]))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | Columns for each Entity |
;;; +----------------------------------------------------------------------------------------------------------------+
(
def
^
:private
all-search-columns
"All columns that will appear in the search results, and the types of those columns. The generated search query is a
`UNION ALL` of the queries for each different entity; it looks something like:
SELECT 'card' AS model, id, cast(NULL AS integer) AS table_id, ...
FROM report_card
UNION ALL
SELECT 'metric' as model, id, table_id, ...
FROM metric
Columns that aren't used in any individual query are replaced with `SELECT cast(NULL AS <type>)` statements. (These
are cast to the appropriate type because Postgres will assume `SELECT NULL` is `TEXT` by default and will refuse to
`UNION` two columns of two different types.)"
(
ordered-map/ordered-map
;; returned for all models
:model
:text
:id
:integer
:name
:text
:description
:text
:archived
:boolean
;; returned for Card, Dashboard, Pulse, and Collection
:collection_id
:integer
;; returned for Card and Dashboard
:collection_position
:integer
:favorite
:boolean
;; returned for Metric and Segment
:table_id
:integer
:database_id
:integer
:table_schema
:text
:table_name
:text
:table_description
:text
))
;; below are the actual columns returned for any given entity
(
def
^
:private
default-columns
"Columns returned for all models."
[
:id
:name
:description
:archived
])
(
def
^
:private
card-columns-without-type
(
concat
default-columns
[
:collection_id
:collection_position
[
:card_fav.id
:favorite
]]))
(
def
^
:private
favorite-col
"Case statement to return boolean values of `:favorite` for Card and Dashboard."
[(
hsql/call
:case
[
:not=
:fave.id
nil
]
true
:else
false
)
:favorite
])
(
def
^
:private
table-columns
"Columns containing information about the table this model references. Returned for Metrics and Segments."
[
:table_id
[
:table.db_id
:database_id
]
[
:table.schema
:table_schema
]
[
:table.name
:table_name
]
[
:table.description
:table_description
]])
(
def
^
:private
dashboard-columns-without-type
(
concat
default-columns
[
:collection_id
:collection_position
[
:dashboard_fav.id
:favorite
]]))
(
defmulti
^
:private
columns-for-model
"The columns that will be returned by the query for `model`, excluding `:model`, which is added automatically."
{
:arglists
'
([
model
])}
class
)
(
def
^
:private
pulse-columns-without-type
(
defmethod
columns-for-model
(
class
Card
)
[
_
]
(
conj
default-columns
:collection_id
:collection_position
favorite-col
))
(
defmethod
columns-for-model
(
class
Dashboard
)
[
_
]
(
conj
default-columns
:collection_id
:collection_position
favorite-col
))
(
defmethod
columns-for-model
(
class
Pulse
)
[
_
]
[
:id
:name
:collection_id
])
(
def
^
:private
collection-columns-without-type
(
concat
default-columns
[[
:id
:collection_id
]]))
(
defmethod
columns-for-model
(
class
Collection
)
[
_
]
(
conj
default-columns
[
:id
:collection_id
]))
(
defmethod
columns-for-model
(
class
Segment
)
[
_
]
(
into
default-columns
table-columns
))
(
defmethod
columns-for-model
(
class
Metric
)
[
_
]
(
into
default-columns
table-columns
))
(
def
^
:private
segment-columns-without-type
default-columns
)
(
def
^
:private
metric-columns-without-type
default-columns
)
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | Shared Query Logic |
;;; +----------------------------------------------------------------------------------------------------------------+
(
defn-
->column
(
s/defn
^
:private
model->alias
:-
s/Keyword
[
model
:-
SearchableModel
]
(
keyword
(
str/lower-case
(
name
model
))))
(
s/defn
^
:private
->column-alias
:-
s/Keyword
"Returns the column name. If the column is aliased, i.e. [`:original_name` `:aliased_name`], return the aliased
column name"
[
column-or-aliased
]
[
column-or-aliased
:-
HoneySQLColumn
]
(
if
(
sequential?
column-or-aliased
)
(
second
column-or-aliased
)
column-or-aliased
))
(
def
^
:private
search-columns-without-type
"The columns found in search query clauses except type. Type is added automatically"
(
set
(
map
->column
(
concat
card-columns-without-type
dashboard-columns-without-type
pulse-columns-without-type
collection-columns-without-type
segment-columns-without-type
metric-columns-without-type
))))
(
s/defn
^
:private
canonical-columns
:-
[
HoneySQLColumn
]
"Returns a seq of canonicalized list of columns for the search query with the given `model` Will return column names
prefixed with the `model` name so that it can be used in criteria. Projects a `nil` for columns the `model` doesn't
have and doesn't modify aliases."
[
model
:-
SearchableModel,
col-alias->honeysql-clause
:-
{
s/Keyword
HoneySQLColumn
}]
(
for
[[
search-col
col-type
]
all-search-columns
:let
[
maybe-aliased-col
(
get
col-alias->honeysql-clause
search-col
)]]
(
cond
(
=
search-col
:model
)
[(
hx/literal
(
name
(
model->alias
model
)))
:model
]
(
def
^
:private
SearchContext
"Map with the various allowed search parameters, used to construct the SQL query"
{
:search-string
(
s/maybe
su/NonBlankString
)
:archived?
s/Bool
:visible-collections
coll/VisibleCollections
})
;; This is an aliased column, no need to include the table alias
(
sequential?
maybe-aliased-col
)
maybe-aliased-col
;; This is a column reference, need to add the table alias to the column
maybe-aliased-col
(
hsql/qualify
(
model->alias
model
)
(
name
maybe-aliased-col
))
(
defn-
make-canonical-columns
"Returns a seq of canonicalized list of columns for the search query with the given `entity-type`. Will return
column names prefixed with the `entity-type` name so that it can be used in criteria. Projects a nil for columns the
`entity-type` doesn't have and doesn't modify aliases."
[
entity-type
col-name->columns
]
(
concat
(
for
[
search-col
search-columns-without-type
:let
[
maybe-aliased-col
(
get
col-name->columns
search-col
)]]
(
cond
;; This is an aliased column, no need to include the table alias
(
sequential?
maybe-aliased-col
)
maybe-aliased-col
;; This is a column reference, need to add the table alias to the column
maybe-aliased-col
(
keyword
(
str
entity-type
"."
(
name
maybe-aliased-col
)))
;; This entity is missing the column, project a null for that column value
:else
[
nil
search-col
]))
[[(
hx/literal
entity-type
)
:model
]]))
(
defn-
merge-search-select
;; This entity is missing the column, project a null for that column value. For Postgres and H2, cast it to the
;; correct type, e.g.
;;
;; SELECT cast(NULL AS integer)
;;
;; For MySQL, this is not needed.
:else
[(
if
(
=
(
mdb/db-type
)
:mysql
)
nil
(
hx/cast
col-type
nil
))
search-col
])))
(
s/defn
^
:private
select-clause-for-model
:-
[
HoneySQLColumn
]
"The search query uses a `union-all` which requires that there be the same number of columns in each of the segments
of the query. This function will take `entity-columns` and will inject constant `nil` values for any column missing
from `entity-columns` but found in `search-columns`"
[
query-map
entity-type
entity-columns
]
(
let
[
col-name->column
(
u/key-by
->column
entity-columns
)
cols-or-nils
(
make-canonical-columns
entity-type
col-name->column
)]
(
apply
h/merge-select
query-map
(
concat
cols-or-nils
))))
;; TODO - not used anywhere except `merge-name-and-archived-search` anymore so we can roll it into that
(
s/defn
^
:private
merge-name-search
"Add case-insensitive name query criteria to `query-map`"
[
query-map
{
:keys
[
search-string
]}
:-
SearchContext
]
(
if
(
empty?
search-string
)
query-map
(
h/merge-where
query-map
[
:like
:%lower.name
(
str
"%"
(
str/lower-case
search-string
)
"%"
)])))
(
s/defn
^
:private
merge-name-and-archived-search
"Add name and archived query criteria to `query-map`"
[
query-map
{
:keys
[
search-string
archived?
]
:as
search-ctx
}
:-
SearchContext
]
(
->
query-map
(
merge-name-search
search-ctx
)
(
h/merge-where
[
:=
:archived
archived?
])))
(
s/defn
^
:private
add-collection-criteria
of the query. This function will take the columns for `model` and will inject constant `nil` values for any column
missing from `entity-columns` but found in `all-search-columns`."
[
model
:-
SearchableModel
]
(
let
[
entity-columns
(
columns-for-model
model
)
column-alias->honeysql-clause
(
u/key-by
->column-alias
entity-columns
)
cols-or-nils
(
canonical-columns
model
column-alias->honeysql-clause
)]
cols-or-nils
))
(
s/defn
^
:private
from-clause-for-model
:-
[(
s/one
[(
s/one
SearchableModel
"model"
)
(
s/one
s/Keyword
"alias"
)]
"from clause"
)]
[
model
:-
SearchableModel
]
[[
model
(
model->alias
model
)]])
(
s/defn
^
:private
base-where-clause-for-model
:-
[(
s/one
(
s/enum
:and
:=
)
"type"
)
s/Any
]
[
model
:-
SearchableModel,
{
:keys
[
search-string
archived?
]}
:-
SearchContext
]
(
let
[
archived-clause
[
:=
(
hsql/qualify
(
model->alias
model
)
:archived
)
archived?
]
search-string-clause
(
when
(
seq
search-string
)
[
:like
(
hsql/call
:lower
(
hsql/qualify
(
model->alias
model
)
:name
))
(
str
"%"
(
str/lower-case
search-string
)
"%"
)])]
(
if
search-string-clause
[
:and
archived-clause
search-string-clause
]
archived-clause
)))
(
s/defn
^
:private
base-query-for-model
:-
{
:select
s/Any,
:from
s/Any,
:where
s/Any
}
"Create a HoneySQL query map with `:select`, `:from`, and `:where` clauses for `model`, suitable for the `UNION ALL`
used in search."
[
model
:-
SearchableModel,
context
:-
SearchContext
]
{
:select
(
select-clause-for-model
model
)
:from
(
from-clause-for-model
model
)
:where
(
base-where-clause-for-model
model
context
)})
(
s/defn
^
:private
add-where-clause-for-collection-id
"Update the query to only include collections the user has access to"
[
query-map,
column-kwd
:-
s/Keyword,
{
:keys
[
visible-collections
]}
:-
SearchContext
]
[
honeysql-query
:-
su/Map,
collection-id-column
:-
s/Keyword,
{
:keys
[
visible-collections
]}
:-
SearchContext
]
(
h/merge-where
query-map
(
coll/visible-collection-ids->honeysql-filter-clause
column-kwd
visible-collections
)))
honeysql-query
(
coll/visible-collection-ids->honeysql-filter-clause
collection-id-column
visible-collections
)))
(
defn-
make-honeysql-search-query
"Create a HoneySQL query map to search for `entity`, suitable for the UNION ALL used in search."
[
entity
search-type
projected-columns
]
(
->
{}
(
merge-search-select
search-type
projected-columns
)
(
h/merge-from
[
entity
(
keyword
search-type
)])))
(
defmulti
^
:private
create-search-query
(
fn
[
entity
search-context
]
entity
))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | Search Queries for each Toucan Model |
;;; +----------------------------------------------------------------------------------------------------------------+
(
s/defmethod
^
:private
create-search-query
:card
(
defmulti
^
:private
search-query-for-model
{
:arglists
'
([
model
search-context
])}
(
fn
[
model
_
]
(
class
model
)))
(
s/defmethod
^
:private
search-query-for-model
(
class
Card
)
[
_
search-ctx
:-
SearchContext
]
(
->
(
make-honeysql-search-query
Card
"card"
card-columns-without-type
)
(
h/left-join
[(
->
(
h/select
:id
:card_id
)
(
h/merge-from
CardFavorite
)
(
h/merge-where
[
:=
:owner_id
*current-user-id*
]))
:card_fav
]
[
:=
:card.id
:card_fav.card_id
])
(
merge-name-and-archived-search
search-ctx
)
(
add-collection-criteria
:collection_id
search-ctx
)))
(
s/defmethod
^
:private
create-search-query
:collection
(
->
(
base-query-for-model
Card
search-ctx
)
(
h/left-join
[
CardFavorite
:fave
]
[
:and
[
:=
:card.id
:fave.card_id
]
[
:=
:fave.owner_id
api/*current-user-id*
]])
(
add-where-clause-for-collection-id
:card.collection_id
search-ctx
)))
(
s/defmethod
^
:private
search-query-for-model
(
class
Collection
)
[
_
search-ctx
:-
SearchContext
]
(
->
(
make-honeysql-search-query
Collection
"collection"
collection-columns-without-type
)
(
merge-name-and-archived-search
search-ctx
)
(
add-collection-criteria
:id
search-ctx
)))
(
->
(
base-query-for-model
Collection
search-ctx
)
(
add-where-clause-for-collection-id
:collection.id
search-ctx
)))
(
s/defmethod
^
:private
create-
search-query
:d
ashboard
(
s/defmethod
^
:private
search-query
-for-model
(
class
D
ashboard
)
[
_
search-ctx
:-
SearchContext
]
(
->
(
make-honeysql-search-query
Dashboard
"dashboard"
dashboard-columns-without-type
)
(
h/left-join
[(
->
(
h/select
:id
:dashboard_id
)
(
h/merge-from
DashboardFavorite
)
(
h/merge-where
[
:=
:user_id
*current-user-id*
]))
:dashboard_fav
]
[
:=
:dashboard.id
:dashboard_fav.dashboard_id
])
(
merge-name-and-archived-search
search-ctx
)
(
add-collection-criteria
:collection_id
search-ctx
)))
(
s/defmethod
^
:private
create-search-query
:pulse
(
->
(
base-query-for-model
Dashboard
search-ctx
)
(
h/left-join
[
DashboardFavorite
:fave
]
[
:and
[
:=
:dashboard.id
:fave.dashboard_id
]
[
:=
:fave.user_id
api/*current-user-id*
]])
(
add-where-clause-for-collection-id
:dashboard.collection_id
search-ctx
)))
(
s/defmethod
^
:private
search-query-for-model
(
class
Pulse
)
[
_
search-ctx
:-
SearchContext
]
;; Pulses don't currently support being archived, omit if archived is true
(
->
(
make-honeysql-search-query
Pulse
"pulse"
pulse-columns-without-type
)
(
merge-name-and-archived-search
search-ctx
)
(
add-collection-criteria
:collection_id
search-ctx
)
(
->
(
base-query-for-model
Pulse
search-ctx
)
(
add-where-clause-for-collection-id
:pulse.collection_id
search-ctx
)
;; We don't want alerts included in pulse results
(
h/merge-where
[
:=
:alert_condition
nil
])))
(
s/defmethod
^
:private
create-
search-query
:m
etric
(
s/defmethod
^
:private
search-query
-for-model
(
class
M
etric
)
[
_
search-ctx
:-
SearchContext
]
(
->
(
make-honeysql-search-query
Metric
"metric"
metric-columns-without-type
)
(
merge-name-and-archived-search
search-ctx
)))
(
->
(
base-query-for-model
Metric
search-ctx
)
(
h/left-join
[
Table
:table
]
[
:=
:metric.table_id
:table.id
]
)))
(
s/defmethod
^
:private
create-
search-query
:s
egment
(
s/defmethod
^
:private
search-query
-for-model
(
class
S
egment
)
[
_
search-ctx
:-
SearchContext
]
(
->
(
make-honeysql-search-query
Segment
"segment"
segment-columns-without-type
)
(
merge-name-and-archived-search
search-ctx
)))
(
defn-
favorited->boolean
[
row
]
(
if-let
[
fav-value
(
get
row
:favorite
)]
(
assoc
row
:favorite
(
and
(
integer?
fav-value
)
(
not
(
zero?
fav-value
))))
row
))
(
->
(
base-query-for-model
Segment
search-ctx
)
(
h/left-join
[
Table
:table
]
[
:=
:segment.table_id
:table.id
])))
(
s/defn
^
:private
search
"Builds a search query that includes all of the searchable entities and runs it"
[
search-ctx
:-
SearchContext
]
(
map
favorited->boolean
(
db/query
{
:union-all
(
for
[
entity
[
:card
:collection
:dashboard
:pulse
:segment
:metric
]
:let
[
query-map
(
create-search-query
entity
search-ctx
)]
:when
query-map
]
query-map
)})))
(
for
[
row
(
db/query
{
:union-all
(
for
[
model
searchable-models
]
(
search-query-for-model
model
search-ctx
))})]
;; MySQL returns `:favorite` as `1` or `0` so convert those to boolean as needed
(
update
row
:favorite
(
fn
[
favorite
]
(
if
(
integer?
favorite
)
(
not
(
zero?
favorite
))
favorite
)))))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | Endpoint |
;;; +----------------------------------------------------------------------------------------------------------------+
(
s/defn
^
:private
make-search-context
:-
SearchContext
[
search-string
:-
(
s/maybe
su/NonBlankString
)
archived-string
:-
(
s/maybe
su/BooleanString
)]
[
search-string
:-
(
s/maybe
su/NonBlankString
)
,
archived-string
:-
(
s/maybe
su/BooleanString
)]
{
:search-string
search-string
:archived?
(
Boolean/parseBoolean
archived-string
)
:visible-collections
(
coll/permissions-set->visible-collection-ids
@
*current-user-permissions-set*
)})
:visible-collections
(
coll/permissions-set->visible-collection-ids
@
api/
*current-user-permissions-set*
)})
(
defendpoint
GET
"/"
(
api/
defendpoint
GET
"/"
"Search Cards, Dashboards, Collections and Pulses for the substring `q`."
[
q
archived
]
{
q
(
s/maybe
su/NonBlankString
)
archived
(
s/maybe
su/BooleanString
)}
(
let
[{
:keys
[
visible-collections
]
:as
search-ctx
}
(
make-search-context
q
archived
)]
;; Throw if the user doesn't have access to any collections
(
check-403
(
or
(
=
:all
visible-collections
)
(
seq
visible-collections
)))
(
api/
check-403
(
or
(
=
:all
visible-collections
)
(
seq
visible-collections
)))
(
search
search-ctx
)))
(
define-routes
)
(
api/
define-routes
)
This diff is collapsed.
Click to expand it.
test/metabase/api/search_test.clj
+
87
−
52
View file @
be0d5b90
...
...
@@ -2,7 +2,7 @@
(
:require
[
clojure
[
set
:as
set
]
[
string
:as
str
]]
[
expectations
:refer
:all
]
[
expectations
:refer
[
expect
]
]
[
metabase.models
[
card
:refer
[
Card
]]
[
card-favorite
:refer
[
CardFavorite
]]
...
...
@@ -14,30 +14,65 @@
[
permissions-group
:as
group
:refer
[
PermissionsGroup
]]
[
permissions-group-membership
:refer
[
PermissionsGroupMembership
]]
[
pulse
:refer
[
Pulse
]]
[
segment
:refer
[
Segment
]]]
[
metabase.test.data.users
:refer
:all
]
[
metabase.test.util
:as
tu
]
[
segment
:refer
[
Segment
]]
[
table
:refer
[
Table
]]]
[
metabase.test
[
data
:as
data
]
[
util
:as
tu
]]
[
metabase.test.data.users
:as
test-users
]
[
metabase.util
:as
u
]
[
toucan.db
:as
db
]
[
toucan.util.test
:as
tt
]))
(
def
default-search-row
{
:description
nil,
:id
true,
:collection_id
false,
:collection_position
nil,
:archived
false,
:favorite
nil
})
(
def
^
:private
default-search-results
(
set
(
map
#
(
merge
default-search-row
%
)
[{
:name
"dashboard test dashboard"
,
:model
"dashboard"
}
{
:name
"collection test collection"
,
:model
"collection"
,
:collection_id
true
}
{
:name
"card test card"
,
:model
"card"
}
{
:name
"pulse test pulse"
,
:model
"pulse"
,
:archived
nil
}
{
:name
"metric test metric"
,
:description
"Lookin' for a blueberry"
,
:model
"metric"
}
{
:name
"segment test segment"
,
:description
"Lookin' for a blueberry"
,
:model
"segment"
}])))
(
def
^
:private
default-metric-segment-results
(
set
(
filter
(
comp
#
{
"metric"
"segment"
}
:model
)
default-search-results
)))
(
def
^
:private
default-archived-results
(
set
(
for
[
result
default-search-results
{
:id
true
:description
nil
:archived
false
:collection_id
false
:collection_position
nil
:favorite
nil
:table_id
false
:database_id
false
:table_schema
nil
:table_name
nil
:table_description
nil
})
(
defn-
table-search-results
"Segments and Metrics come back with information about their Tables as of 0.33.0. The `model-defaults` for Segment and
Metric put them both in the `:checkins` Table."
[]
(
merge
{
:table_id
true,
:database_id
true
}
(
db/select-one
[
Table
[
:name
:table_name
]
[
:schema
:table_schema
]
[
:description
:table_description
]]
:id
(
data/id
:checkins
))))
(
defn-
default-search-results
[]
#
{(
merge
default-search-row
{
:name
"dashboard test dashboard"
,
:model
"dashboard"
,
:favorite
false
})
(
merge
default-search-row
{
:name
"collection test collection"
,
:model
"collection"
,
:collection_id
true
})
(
merge
default-search-row
{
:name
"card test card"
,
:model
"card"
,
:favorite
false
})
(
merge
default-search-row
{
:name
"pulse test pulse"
,
:model
"pulse"
,
:archived
nil
})
(
merge
default-search-row
{
:model
"metric"
,
:name
"metric test metric"
,
:description
"Lookin' for a blueberry"
}
(
table-search-results
))
(
merge
default-search-row
{
:model
"segment"
,
:name
"segment test segment"
,
:description
"Lookin' for a blueberry"
}
(
table-search-results
))})
(
defn-
default-metric-segment-results
[]
(
set
(
filter
(
comp
#
{
"metric"
"segment"
}
:model
)
(
default-search-results
))))
(
defn-
default-archived-results
[]
(
set
(
for
[
result
(
default-search-results
)
:when
(
false?
(
:archived
result
))]
(
assoc
result
:archived
true
))))
...
...
@@ -47,10 +82,10 @@
(
f
search-item
)
search-item
))))
(
def
^
:private
default-results-with-collection
(
def
n-
default-results-with-collection
[]
(
on-search-types
#
{
"dashboard"
"pulse"
"card"
}
#
(
assoc
%
:collection_id
true
)
default-search-results
))
(
default-search-results
))
)
(
defn-
do-with-search-items
[
search-string
in-root-collection?
f
]
(
let
[
data-map
(
fn
[
instance-name
]
...
...
@@ -73,17 +108,17 @@
:segment
segment
}))))
(
defmacro
^
:private
with-search-items-in-root-collection
[
search-string
&
body
]
`
(
do-with-search-items
~
search-string
true
(
fn
[
_
#
]
~@
body
)))
`
(
do-with-search-items
~
search-string
true
(
fn
[
~
'
_
]
~@
body
)))
(
defmacro
^
:private
with-search-items-in-collection
[
created-items-sym
search-string
&
body
]
`
(
do-with-search-items
~
search-string
false
(
fn
[
~
created-items-sym
]
~@
body
)))
(
defn-
search-request
[
user-kwd
&
params
]
(
tu/boolean-ids-and-timestamps
(
set
(
apply
(
user->client
user-kwd
)
:get
200
"search"
params
))))
(
tu/boolean-ids-and-timestamps
(
set
(
apply
(
test-users/
user->client
user-kwd
)
:get
200
"search"
params
))))
;; Basic search, should find 1 of each entity type, all items in the root collection
(
expect
default-search-results
(
default-search-results
)
(
with-search-items-in-root-collection
"test"
(
search-request
:crowberto
:q
"test"
)))
...
...
@@ -91,110 +126,110 @@
;; previous tests. Instead of an = comparison here, just ensure our default results are included
(
expect
(
set/subset?
default-search-results
(
default-search-results
)
(
with-search-items-in-root-collection
"test"
(
search-request
:crowberto
))))
;; Ensure that users without perms for the root collection don't get results
;; NOTE: Metrics and segments don't have collections, so they'll be returned
(
expect
default-metric-segment-results
(
default-metric-segment-results
)
(
tu/with-non-admin-groups-no-root-collection-perms
(
with-search-items-in-root-collection
"test"
(
search-request
:rasta
:q
"test"
))))
;; Users that have root collection permissions should get root collection search results
(
expect
(
set
(
remove
(
comp
#
{
"collection"
}
:model
)
default-search-results
))
(
set
(
remove
(
comp
#
{
"collection"
}
:model
)
(
default-search-results
))
)
(
tu/with-non-admin-groups-no-root-collection-perms
(
with-search-items-in-root-collection
"test"
(
tt/with-temp*
[
PermissionsGroup
[
group
]
PermissionsGroupMembership
[
_
{
:user_id
(
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
PermissionsGroupMembership
[
_
{
:user_id
(
test-users/
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
(
perms/grant-permissions!
group
(
perms/collection-read-path
{
:metabase.models.collection/is-root?
true
}))
(
search-request
:rasta
:q
"test"
)))))
;; Users without root collection permissions should still see other collections they have access to
(
expect
(
into
default-results-with-collection
(
map
#
(
merge
default-search-row
%
)
(
into
(
default-results-with-collection
)
(
map
#
(
merge
default-search-row
%
(
table-search-results
)
)
[{
:name
"metric test2 metric"
,
:description
"Lookin' for a blueberry"
,
:model
"metric"
}
{
:name
"segment test2 segment"
,
:description
"Lookin' for a blueberry"
,
:model
"segment"
}]))
(
tu/with-non-admin-groups-no-root-collection-perms
(
with-search-items-in-collection
{
:keys
[
collection
]}
"test"
(
with-search-items-in-root-collection
"test2"
(
tt/with-temp*
[
PermissionsGroup
[
group
]
PermissionsGroupMembership
[
_
{
:user_id
(
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
PermissionsGroupMembership
[
_
{
:user_id
(
test-users/
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
(
perms/grant-collection-read-permissions!
group
(
u/get-id
collection
))
(
search-request
:rasta
:q
"test"
))))))
;; Users with root collection permissions should be able to search root collection data long with collections they
;; have access to
(
expect
(
into
default-results-with-collection
(
for
[
row
default-search-results
(
into
(
default-results-with-collection
)
(
for
[
row
(
default-search-results
)
:when
(
not=
"collection"
(
:model
row
))]
(
update
row
:name
#
(
str/replace
%
"test"
"test2"
))))
(
tu/with-non-admin-groups-no-root-collection-perms
(
with-search-items-in-collection
{
:keys
[
collection
]}
"test"
(
with-search-items-in-root-collection
"test2"
(
tt/with-temp*
[
PermissionsGroup
[
group
]
PermissionsGroupMembership
[
_
{
:user_id
(
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
PermissionsGroupMembership
[
_
{
:user_id
(
test-users/
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
(
perms/grant-permissions!
group
(
perms/collection-read-path
{
:metabase.models.collection/is-root?
true
}))
(
perms/grant-collection-read-permissions!
group
collection
)
(
search-request
:rasta
:q
"test"
))))))
;; Users with access to multiple collections should see results from all collections they have access to
(
expect
(
into
default-results-with-collection
(
into
(
default-results-with-collection
)
(
map
(
fn
[
row
]
(
update
row
:name
#
(
str/replace
%
"test"
"test2"
)))
default-results-with-collection
))
(
default-results-with-collection
))
)
(
with-search-items-in-collection
{
coll-1
:collection
}
"test"
(
with-search-items-in-collection
{
coll-2
:collection
}
"test2"
(
tt/with-temp*
[
PermissionsGroup
[
group
]
PermissionsGroupMembership
[
_
{
:user_id
(
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
PermissionsGroupMembership
[
_
{
:user_id
(
test-users/
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
(
perms/grant-collection-read-permissions!
group
(
u/get-id
coll-1
))
(
perms/grant-collection-read-permissions!
group
(
u/get-id
coll-2
))
(
search-request
:rasta
:q
"test"
)))))
;; User should only see results in the collection they have access to
(
expect
(
into
default-results-with-collection
(
map
#
(
merge
default-search-row
%
)
(
into
(
default-results-with-collection
)
(
map
#
(
merge
default-search-row
%
(
table-search-results
)
)
[{
:name
"metric test2 metric"
,
:description
"Lookin' for a blueberry"
,
:model
"metric"
}
{
:name
"segment test2 segment"
,
:description
"Lookin' for a blueberry"
,
:model
"segment"
}]))
(
tu/with-non-admin-groups-no-root-collection-perms
(
with-search-items-in-collection
{
coll-1
:collection
}
"test"
(
with-search-items-in-collection
{
coll-2
:collection
}
"test2"
(
tt/with-temp*
[
PermissionsGroup
[
group
]
PermissionsGroupMembership
[
_
{
:user_id
(
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
PermissionsGroupMembership
[
_
{
:user_id
(
test-users/
user->id
:rasta
)
,
:group_id
(
u/get-id
group
)}]]
(
perms/grant-collection-read-permissions!
group
(
u/get-id
coll-1
))
(
search-request
:rasta
:q
"test"
))))))
;; Favorites are per user, so other user's favorites don't cause search results to be favorited
(
expect
default-results-with-collection
(
default-results-with-collection
)
(
with-search-items-in-collection
{
:keys
[
card
dashboard
]}
"test"
(
tt/with-temp*
[
CardFavorite
[
_
{
:card_id
(
u/get-id
card
)
:owner_id
(
user->id
:rasta
)}]
:owner_id
(
test-users/
user->id
:rasta
)}]
DashboardFavorite
[
_
{
:dashboard_id
(
u/get-id
dashboard
)
:user_id
(
user->id
:rasta
)}]]
:user_id
(
test-users/
user->id
:rasta
)}]]
(
search-request
:crowberto
:q
"test"
))))
;; Basic search, should find 1 of each entity type and include favorites when available
(
expect
(
on-search-types
#
{
"dashboard"
"card"
}
#
(
assoc
%
:favorite
true
)
default-results-with-collection
)
(
default-results-with-collection
)
)
(
with-search-items-in-collection
{
:keys
[
card
dashboard
]}
"test"
(
tt/with-temp*
[
CardFavorite
[
_
{
:card_id
(
u/get-id
card
)
:owner_id
(
user->id
:crowberto
)}]
:owner_id
(
test-users/
user->id
:crowberto
)}]
DashboardFavorite
[
_
{
:dashboard_id
(
u/get-id
dashboard
)
:user_id
(
user->id
:crowberto
)}]]
:user_id
(
test-users/
user->id
:crowberto
)}]]
(
search-request
:crowberto
:q
"test"
))))
;; Basic search should only return substring matches
(
expect
default-search-results
(
default-search-results
)
(
with-search-items-in-root-collection
"test"
(
with-search-items-in-root-collection
"something different"
(
search-request
:crowberto
:q
"test"
))))
...
...
@@ -204,7 +239,7 @@
;; Should return unarchived results by default
(
expect
default-search-results
(
default-search-results
)
(
with-search-items-in-root-collection
"test"
(
tt/with-temp*
[
Card
[
_
(
archived
{
:name
"card test card 2"
})]
Dashboard
[
_
(
archived
{
:name
"dashboard test dashboard 2"
})]
...
...
@@ -215,7 +250,7 @@
;; Should return archived results when specified
(
expect
default-archived-results
(
default-archived-results
)
(
with-search-items-in-root-collection
"test2"
(
tt/with-temp*
[
Card
[
_
(
archived
{
:name
"card test card"
})]
Dashboard
[
_
(
archived
{
:name
"dashboard test dashboard"
})]
...
...
@@ -235,4 +270,4 @@
(
filter
(
fn
[{
:keys
[
model
id
]}]
(
and
(
=
id
(
u/get-id
pulse
))
(
=
"pulse"
model
)))
((
user->client
:crowberto
)
:get
200
"search"
)))))
((
test-users/
user->client
:crowberto
)
:get
200
"search"
)))))
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment