Commit dc607b8e by Torkel Ödegaard

Dashboard search now supports filtering by multiple dashboard tags, Closes #2095

parent 1a71da41
...@@ -12,6 +12,7 @@ ...@@ -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 ( ...@@ -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) { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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 } ...@@ -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 { ...@@ -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 { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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,11 +15,14 @@ ...@@ -15,11 +15,14 @@
<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"> |
<i class="fa fa-remove"></i> <span ng-repeat="tagName in query.tag">
{{query.tag}} <a ng-click="removeTag(tagName, $event)" tag-color-from-name="tagName" class="label label-tag">
</a> <i class="fa fa-remove"></i>
{{tagName}}
</a>
</span>
</span> </span>
</div> </div>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment