mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard search now supports filtering by multiple dashboard tags, Closes #2095
This commit is contained in:
parent
1a71da417c
commit
dc607b8e8a
@ -12,6 +12,7 @@
|
|||||||
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
|
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
|
||||||
|
|
||||||
**Backend**
|
**Backend**
|
||||||
|
- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags
|
||||||
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
|
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
|
||||||
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
|
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
|
||||||
- [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images
|
- [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func Search(c *middleware.Context) {
|
func Search(c *middleware.Context) {
|
||||||
query := c.Query("query")
|
query := c.Query("query")
|
||||||
tag := c.Query("tag")
|
tags := c.QueryStrings("tag")
|
||||||
starred := c.Query("starred")
|
starred := c.Query("starred")
|
||||||
limit := c.QueryInt("limit")
|
limit := c.QueryInt("limit")
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ func Search(c *middleware.Context) {
|
|||||||
|
|
||||||
searchQuery := search.Query{
|
searchQuery := search.Query{
|
||||||
Title: query,
|
Title: query,
|
||||||
Tag: tag,
|
Tags: tags,
|
||||||
UserId: c.UserId,
|
UserId: c.UserId,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
IsStarred: starred == "true",
|
IsStarred: starred == "true",
|
||||||
|
@ -33,7 +33,6 @@ func searchHandler(query *Query) error {
|
|||||||
|
|
||||||
dashQuery := FindPersistedDashboardsQuery{
|
dashQuery := FindPersistedDashboardsQuery{
|
||||||
Title: query.Title,
|
Title: query.Title,
|
||||||
Tag: query.Tag,
|
|
||||||
UserId: query.UserId,
|
UserId: query.UserId,
|
||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
IsStarred: query.IsStarred,
|
IsStarred: query.IsStarred,
|
||||||
@ -55,6 +54,22 @@ func searchHandler(query *Query) error {
|
|||||||
hits = append(hits, jsonHits...)
|
hits = append(hits, jsonHits...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter out results with tag filter
|
||||||
|
if len(query.Tags) > 0 {
|
||||||
|
filtered := HitList{}
|
||||||
|
for _, hit := range hits {
|
||||||
|
if hasRequiredTags(query.Tags, hit.Tags) {
|
||||||
|
filtered = append(filtered, hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort tags
|
||||||
|
for _, hit := range hits {
|
||||||
|
sort.Strings(hit.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
// add isStarred info
|
// add isStarred info
|
||||||
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
|
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -63,15 +78,29 @@ func searchHandler(query *Query) error {
|
|||||||
// sort main result array
|
// sort main result array
|
||||||
sort.Sort(hits)
|
sort.Sort(hits)
|
||||||
|
|
||||||
// sort tags
|
|
||||||
for _, hit := range hits {
|
|
||||||
sort.Strings(hit.Tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Result = hits
|
query.Result = hits
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stringInSlice(a string, list []string) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasRequiredTags(queryTags, hitTags []string) bool {
|
||||||
|
for _, queryTag := range queryTags {
|
||||||
|
if !stringInSlice(queryTag, hitTags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
|
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
|
||||||
query := m.GetUserStarsQuery{UserId: userId}
|
query := m.GetUserStarsQuery{UserId: userId}
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
@ -45,5 +45,17 @@ func TestSearch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("That filters by tag", func() {
|
||||||
|
query.Tags = []string{"BB", "AA"}
|
||||||
|
err := searchHandler(&query)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("should return correct results", func() {
|
||||||
|
So(len(query.Result), ShouldEqual, 2)
|
||||||
|
So(query.Result[0].Title, ShouldEqual, "BBAA")
|
||||||
|
So(query.Result[1].Title, ShouldEqual, "CCAA")
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -56,13 +56,6 @@ func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter out results with tag filter
|
|
||||||
if query.Tag != "" {
|
|
||||||
if !strings.Contains(item.TagsCsv, query.Tag) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add results with matchig title filter
|
// add results with matchig title filter
|
||||||
if strings.Contains(item.TitleLower, query.Title) {
|
if strings.Contains(item.TitleLower, query.Title) {
|
||||||
results = append(results, &Hit{
|
results = append(results, &Hit{
|
||||||
|
@ -17,14 +17,14 @@ func TestJsonDashIndex(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to search index", func() {
|
Convey("Should be able to search index", func() {
|
||||||
res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20})
|
res, err := index.Search(&Query{Title: "", Limit: 20})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(res), ShouldEqual, 3)
|
So(len(res), ShouldEqual, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to search index by title", func() {
|
Convey("Should be able to search index by title", func() {
|
||||||
res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20})
|
res, err := index.Search(&Query{Title: "home", Limit: 20})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(res), ShouldEqual, 1)
|
So(len(res), ShouldEqual, 1)
|
||||||
@ -32,7 +32,7 @@ func TestJsonDashIndex(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should not return when starred is filtered", func() {
|
Convey("Should not return when starred is filtered", func() {
|
||||||
res, err := index.Search(&Query{Title: "", Tag: "", IsStarred: true})
|
res, err := index.Search(&Query{Title: "", IsStarred: true})
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(res), ShouldEqual, 0)
|
So(len(res), ShouldEqual, 0)
|
||||||
|
@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Title string
|
Title string
|
||||||
Tag string
|
Tags []string
|
||||||
OrgId int64
|
OrgId int64
|
||||||
UserId int64
|
UserId int64
|
||||||
Limit int
|
Limit int
|
||||||
@ -37,7 +37,6 @@ type Query struct {
|
|||||||
|
|
||||||
type FindPersistedDashboardsQuery struct {
|
type FindPersistedDashboardsQuery struct {
|
||||||
Title string
|
Title string
|
||||||
Tag string
|
|
||||||
OrgId int64
|
OrgId int64
|
||||||
UserId int64
|
UserId int64
|
||||||
Limit int
|
Limit int
|
||||||
|
@ -150,13 +150,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
|||||||
params = append(params, "%"+query.Title+"%")
|
params = append(params, "%"+query.Title+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query.Tag) > 0 {
|
|
||||||
sql.WriteString(" AND dashboard_tag.term=?")
|
|
||||||
params = append(params, query.Tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Limit == 0 || query.Limit > 10000 {
|
if query.Limit == 0 || query.Limit > 10000 {
|
||||||
query.Limit = 300
|
query.Limit = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit))
|
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit))
|
||||||
|
@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
So(len(hit.Tags), ShouldEqual, 2)
|
So(len(hit.Tags), ShouldEqual, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to search for dashboards using tags", func() {
|
|
||||||
query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1}
|
|
||||||
query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1}
|
|
||||||
|
|
||||||
err := SearchDashboards(&query1)
|
|
||||||
err = SearchDashboards(&query2)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
So(len(query1.Result), ShouldEqual, 1)
|
|
||||||
So(len(query2.Result), ShouldEqual, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Should not be able to save dashboard with same name", func() {
|
Convey("Should not be able to save dashboard with same name", func() {
|
||||||
cmd := m.SaveDashboardCommand{
|
cmd := m.SaveDashboardCommand{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
|
@ -14,7 +14,7 @@ function (angular, _, config) {
|
|||||||
$scope.giveSearchFocus = 0;
|
$scope.giveSearchFocus = 0;
|
||||||
$scope.selectedIndex = -1;
|
$scope.selectedIndex = -1;
|
||||||
$scope.results = [];
|
$scope.results = [];
|
||||||
$scope.query = { query: '', tag: '', starred: false };
|
$scope.query = { query: '', tag: [], starred: false };
|
||||||
$scope.currentSearchId = 0;
|
$scope.currentSearchId = 0;
|
||||||
|
|
||||||
if ($scope.dashboardViewState.fullscreen) {
|
if ($scope.dashboardViewState.fullscreen) {
|
||||||
@ -82,12 +82,11 @@ function (angular, _, config) {
|
|||||||
|
|
||||||
$scope.queryHasNoFilters = function() {
|
$scope.queryHasNoFilters = function() {
|
||||||
var query = $scope.query;
|
var query = $scope.query;
|
||||||
return query.query === '' && query.starred === false && query.tag === '';
|
return query.query === '' && query.starred === false && query.tag.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.filterByTag = function(tag, evt) {
|
$scope.filterByTag = function(tag, evt) {
|
||||||
$scope.query.tag = tag;
|
$scope.query.tag.push(tag);
|
||||||
$scope.query.tagcloud = false;
|
|
||||||
$scope.search();
|
$scope.search();
|
||||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||||
if (evt) {
|
if (evt) {
|
||||||
@ -96,6 +95,14 @@ function (angular, _, config) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.removeTag = function(tag, evt) {
|
||||||
|
$scope.query.tag = _.without($scope.query.tag, tag);
|
||||||
|
$scope.search();
|
||||||
|
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getTags = function() {
|
$scope.getTags = function() {
|
||||||
return backendSrv.get('/api/dashboards/tags').then(function(results) {
|
return backendSrv.get('/api/dashboards/tags').then(function(results) {
|
||||||
$scope.tagsMode = true;
|
$scope.tagsMode = true;
|
||||||
|
@ -15,12 +15,15 @@
|
|||||||
<i class="fa fa-remove" ng-show="tagsMode"></i>
|
<i class="fa fa-remove" ng-show="tagsMode"></i>
|
||||||
tags
|
tags
|
||||||
</a>
|
</a>
|
||||||
<span ng-show="query.tag">
|
<span ng-if="query.tag.length">
|
||||||
| <a ng-click="filterByTag('')" tag-color-from-name="query.tag" class="label label-tag" ng-if="query.tag">
|
|
|
||||||
|
<span ng-repeat="tagName in query.tag">
|
||||||
|
<a ng-click="removeTag(tagName, $event)" tag-color-from-name="tagName" class="label label-tag">
|
||||||
<i class="fa fa-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
{{query.tag}}
|
{{tagName}}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user