Browse Source

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

Torkel Ödegaard 10 years ago
parent
commit
dc607b8e8a

+ 1 - 0
CHANGELOG.md

@@ -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
 
 **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 #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

+ 2 - 2
pkg/api/search.go

@@ -8,7 +8,7 @@ import (
 
 func Search(c *middleware.Context) {
 	query := c.Query("query")
-	tag := c.Query("tag")
+	tags := c.QueryStrings("tag")
 	starred := c.Query("starred")
 	limit := c.QueryInt("limit")
 
@@ -18,7 +18,7 @@ func Search(c *middleware.Context) {
 
 	searchQuery := search.Query{
 		Title:     query,
-		Tag:       tag,
+		Tags:      tags,
 		UserId:    c.UserId,
 		Limit:     limit,
 		IsStarred: starred == "true",

+ 35 - 6
pkg/search/handlers.go

@@ -33,7 +33,6 @@ func searchHandler(query *Query) error {
 
 	dashQuery := FindPersistedDashboardsQuery{
 		Title:     query.Title,
-		Tag:       query.Tag,
 		UserId:    query.UserId,
 		Limit:     query.Limit,
 		IsStarred: query.IsStarred,
@@ -55,6 +54,22 @@ func searchHandler(query *Query) error {
 		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
 	if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
 		return err
@@ -63,15 +78,29 @@ func searchHandler(query *Query) error {
 	// sort main result array
 	sort.Sort(hits)
 
-	// sort tags
-	for _, hit := range hits {
-		sort.Strings(hit.Tags)
-	}
-
 	query.Result = hits
 	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 {
 	query := m.GetUserStarsQuery{UserId: userId}
 	if err := bus.Dispatch(&query); err != nil {

+ 12 - 0
pkg/search/handlers_test.go

@@ -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")
+			})
+
+		})
 	})
 }

+ 0 - 7
pkg/search/json_index.go

@@ -56,13 +56,6 @@ func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
 			break
 		}
 
-		// filter out results with tag filter
-		if query.Tag != "" {
-			if !strings.Contains(item.TagsCsv, query.Tag) {
-				continue
-			}
-		}
-
 		// add results with matchig title filter
 		if strings.Contains(item.TitleLower, query.Title) {
 			results = append(results, &Hit{

+ 3 - 3
pkg/search/json_index_test.go

@@ -17,14 +17,14 @@ func TestJsonDashIndex(t *testing.T) {
 		})
 
 		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(len(res), ShouldEqual, 3)
 		})
 
 		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(len(res), ShouldEqual, 1)
@@ -32,7 +32,7 @@ func TestJsonDashIndex(t *testing.T) {
 		})
 
 		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(len(res), ShouldEqual, 0)

+ 1 - 2
pkg/search/models.go

@@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
 
 type Query struct {
 	Title     string
-	Tag       string
+	Tags      []string
 	OrgId     int64
 	UserId    int64
 	Limit     int
@@ -37,7 +37,6 @@ type Query struct {
 
 type FindPersistedDashboardsQuery struct {
 	Title     string
-	Tag       string
 	OrgId     int64
 	UserId    int64
 	Limit     int

+ 1 - 6
pkg/services/sqlstore/dashboard.go

@@ -150,13 +150,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 		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 {
-		query.Limit = 300
+		query.Limit = 1000
 	}
 
 	sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit))

+ 0 - 12
pkg/services/sqlstore/dashboard_test.go

@@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) {
 				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() {
 				cmd := m.SaveDashboardCommand{
 					OrgId: 1,

+ 11 - 4
public/app/controllers/search.js

@@ -14,7 +14,7 @@ function (angular, _, config) {
       $scope.giveSearchFocus = 0;
       $scope.selectedIndex = -1;
       $scope.results = [];
-      $scope.query = { query: '', tag: '', starred: false };
+      $scope.query = { query: '', tag: [], starred: false };
       $scope.currentSearchId = 0;
 
       if ($scope.dashboardViewState.fullscreen) {
@@ -82,12 +82,11 @@ function (angular, _, config) {
 
     $scope.queryHasNoFilters = function() {
       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.query.tag = tag;
-      $scope.query.tagcloud = false;
+      $scope.query.tag.push(tag);
       $scope.search();
       $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
       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() {
       return backendSrv.get('/api/dashboards/tags').then(function(results) {
         $scope.tagsMode = true;

+ 8 - 5
public/app/partials/search.html

@@ -15,11 +15,14 @@
 				<i class="fa fa-remove" ng-show="tagsMode"></i>
 				tags
 			</a>
-			<span ng-show="query.tag">
-				| <a ng-click="filterByTag('')" tag-color-from-name="query.tag"  class="label label-tag" ng-if="query.tag">
-					<i class="fa fa-remove"></i>
-					{{query.tag}}
-				</a>
+			<span ng-if="query.tag.length">
+				|
+				<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>
+						{{tagName}}
+					</a>
+				</span>
 			</span>
 		</div>
 	</div>