Ver Fonte

dashboard folder search fix

Torkel Ödegaard há 8 anos atrás
pai
commit
fc69d59cae

+ 4 - 4
pkg/api/dashboard.go

@@ -88,13 +88,13 @@ func GetDashboard(c *middleware.Context) Response {
 		Version:     dash.Version,
 		HasAcl:      dash.HasAcl,
 		IsFolder:    dash.IsFolder,
-		FolderId:    dash.ParentId,
+		FolderId:    dash.FolderId,
 		FolderTitle: "Root",
 	}
 
 	// lookup folder title
-	if dash.ParentId > 0 {
-		query := m.GetDashboardQuery{Id: dash.ParentId, OrgId: c.OrgId}
+	if dash.FolderId > 0 {
+		query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
 		if err := bus.Dispatch(&query); err != nil {
 			return ApiError(500, "Dashboard folder could not be read", err)
 		}
@@ -170,7 +170,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 		return dashboardGuardianResponse(err)
 	}
 
-	if dash.IsFolder && dash.ParentId > 0 {
+	if dash.IsFolder && dash.FolderId > 0 {
 		return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
 	}
 

+ 7 - 7
pkg/api/dashboard_test.go

@@ -22,7 +22,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 	Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
 		fakeDash := m.NewDashboard("Child dash")
 		fakeDash.Id = 1
-		fakeDash.ParentId = 1
+		fakeDash.FolderId = 1
 		fakeDash.HasAcl = false
 
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
@@ -50,7 +50,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 
 		cmd := m.SaveDashboardCommand{
 			Dashboard: simplejson.NewFromAny(map[string]interface{}{
-				"parentId": fakeDash.ParentId,
+				"folderId": fakeDash.FolderId,
 				"title":    fakeDash.Title,
 				"id":       fakeDash.Id,
 			}),
@@ -163,10 +163,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
 					return nil
 				})
 				invalidCmd := m.SaveDashboardCommand{
-					ParentId: fakeDash.ParentId,
+					FolderId: fakeDash.FolderId,
 					IsFolder: true,
 					Dashboard: simplejson.NewFromAny(map[string]interface{}{
-						"parentId": fakeDash.ParentId,
+						"folderId": fakeDash.FolderId,
 						"title":    fakeDash.Title,
 					}),
 				}
@@ -183,7 +183,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 	Convey("Given a dashboard with a parent folder which has an acl", t, func() {
 		fakeDash := m.NewDashboard("Child dash")
 		fakeDash.Id = 1
-		fakeDash.ParentId = 1
+		fakeDash.FolderId = 1
 		fakeDash.HasAcl = true
 
 		aclMockResp := []*m.DashboardAclInfoDTO{
@@ -210,10 +210,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		})
 
 		cmd := m.SaveDashboardCommand{
-			ParentId: fakeDash.ParentId,
+			FolderId: fakeDash.FolderId,
 			Dashboard: simplejson.NewFromAny(map[string]interface{}{
 				"id":       fakeDash.Id,
-				"parentId": fakeDash.ParentId,
+				"folderId": fakeDash.FolderId,
 				"title":    fakeDash.Title,
 			}),
 		}

+ 3 - 3
pkg/models/dashboards.go

@@ -48,7 +48,7 @@ type Dashboard struct {
 
 	UpdatedBy int64
 	CreatedBy int64
-	ParentId  int64
+	FolderId  int64
 	IsFolder  bool
 	HasAcl    bool
 
@@ -116,7 +116,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 	dash.OrgId = cmd.OrgId
 	dash.PluginId = cmd.PluginId
 	dash.IsFolder = cmd.IsFolder
-	dash.ParentId = cmd.ParentId
+	dash.FolderId = cmd.FolderId
 	dash.UpdateSlug()
 	return dash
 }
@@ -144,7 +144,7 @@ type SaveDashboardCommand struct {
 	OrgId        int64            `json:"-"`
 	RestoredFrom int              `json:"-"`
 	PluginId     string           `json:"-"`
-	ParentId     int64            `json:"parentId"`
+	FolderId     int64            `json:"folderId"`
 	IsFolder     bool             `json:"isFolder"`
 
 	Result *Dashboard

+ 3 - 3
pkg/models/dashboards_test.go

@@ -44,11 +44,11 @@ func TestDashboardModel(t *testing.T) {
 		json := simplejson.New()
 		json.Set("title", "test dash")
 
-		cmd := &SaveDashboardCommand{Dashboard: json, ParentId: 1}
+		cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
 		dash := cmd.GetDashboardModel()
 
-		Convey("Should set ParentId", func() {
-			So(dash.ParentId, ShouldEqual, 1)
+		Convey("Should set FolderId", func() {
+			So(dash.FolderId, ShouldEqual, 1)
 		})
 	})
 }

+ 1 - 1
pkg/services/search/handlers.go

@@ -44,7 +44,7 @@ func searchHandler(query *Query) error {
 		IsStarred:    query.IsStarred,
 		DashboardIds: query.DashboardIds,
 		Type:         query.Type,
-		ParentId:     query.FolderId,
+		FolderId:     query.FolderId,
 		Mode:         query.Mode,
 	}
 

+ 1 - 3
pkg/services/search/handlers_test.go

@@ -20,9 +20,7 @@ func TestSearch(t *testing.T) {
 				&Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}},
 				&Hit{Id: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
 				&Hit{Id: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
-				&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder", Dashboards: []Hit{
-					{Id: 18, Title: "ZZAA", Tags: []string{"ZZ"}},
-				}},
+				&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"},
 			}
 			return nil
 		})

+ 10 - 9
pkg/services/search/models.go

@@ -14,14 +14,15 @@ const (
 )
 
 type Hit struct {
-	Id         int64    `json:"id"`
-	Title      string   `json:"title"`
-	Uri        string   `json:"uri"`
-	Type       HitType  `json:"type"`
-	Tags       []string `json:"tags"`
-	IsStarred  bool     `json:"isStarred"`
-	ParentId   int64    `json:"parentId"`
-	Dashboards []Hit    `json:"dashboards"`
+	Id          int64    `json:"id"`
+	Title       string   `json:"title"`
+	Uri         string   `json:"uri"`
+	Type        HitType  `json:"type"`
+	Tags        []string `json:"tags"`
+	IsStarred   bool     `json:"isStarred"`
+	FolderId    int64    `json:"folderId,omitempty"`
+	FolderTitle string   `json:"folderTitle,omitempty"`
+	FolderSlug  string   `json:"folderSlug,omitempty"`
 }
 
 type HitList []*Hit
@@ -62,7 +63,7 @@ type FindPersistedDashboardsQuery struct {
 	IsStarred    bool
 	DashboardIds []int64
 	Type         string
-	ParentId     int64
+	FolderId     int64
 	Mode         string
 
 	Result HitList

+ 17 - 70
pkg/services/sqlstore/dashboard.go

@@ -81,7 +81,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 		} else {
 			dash.Version += 1
 			dash.Data.Set("version", dash.Version)
-			affectedRows, err = sess.MustCols("parent_id").Id(dash.Id).Update(dash)
+			affectedRows, err = sess.MustCols("folder_id").Id(dash.Id).Update(dash)
 		}
 
 		if err != nil {
@@ -153,7 +153,7 @@ type DashboardSearchProjection struct {
 	Slug        string
 	Term        string
 	IsFolder    bool
-	ParentId    int64
+	FolderId    int64
 	FolderSlug  string
 	FolderTitle string
 }
@@ -168,11 +168,11 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 					  dashboard.slug,
 					  dashboard_tag.term,
             dashboard.is_folder,
-            dashboard.parent_id,
+            dashboard.folder_id,
 			      f.slug as folder_slug,
 			      f.title as folder_title
 					FROM dashboard
-					LEFT OUTER JOIN dashboard f on f.id = dashboard.parent_id
+					LEFT OUTER JOIN dashboard f on f.id = dashboard.folder_id
 					LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
 
 	if query.IsStarred {
@@ -204,7 +204,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 		allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
 		SELECT distinct d.id AS DashboardId
 			FROM dashboard AS d
-	      LEFT JOIN dashboard_acl as da on d.parent_id = da.dashboard_id or d.id = da.dashboard_id
+	      LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
 	      LEFT JOIN user_group_member as ugm on ugm.user_group_id =  da.user_group_id
 	      LEFT JOIN org_user ou on ou.role = da.role
 			WHERE
@@ -230,9 +230,9 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 		sql.WriteString(" AND dashboard.is_folder = 0")
 	}
 
-	if query.ParentId > 0 {
-		sql.WriteString(" AND dashboard.parent_id = ?")
-		params = append(params, query.ParentId)
+	if query.FolderId > 0 {
+		sql.WriteString(" AND dashboard.folder_id = ?")
+		params = append(params, query.FolderId)
 	}
 
 	sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
@@ -253,38 +253,11 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 		return err
 	}
 
-	if query.Mode == "tree" {
-		res, err = appendDashboardFolders(res)
-		if err != nil {
-			return err
-		}
-	}
-
 	makeQueryResult(query, res)
 
-	if query.Mode == "tree" {
-		convertToDashboardFolders(query)
-	}
-
 	return nil
 }
 
-// appends parent folders for any hits to the search result
-func appendDashboardFolders(res []DashboardSearchProjection) ([]DashboardSearchProjection, error) {
-	for _, item := range res {
-		if item.ParentId > 0 {
-			res = append(res, DashboardSearchProjection{
-				Id:       item.ParentId,
-				IsFolder: true,
-				Slug:     item.FolderSlug,
-				Title:    item.FolderTitle,
-			})
-		}
-	}
-
-	return res, nil
-}
-
 func getHitType(item DashboardSearchProjection) search.HitType {
 	var hitType search.HitType
 	if item.IsFolder {
@@ -304,12 +277,14 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 		hit, exists := hits[item.Id]
 		if !exists {
 			hit = &search.Hit{
-				Id:       item.Id,
-				Title:    item.Title,
-				Uri:      "db/" + item.Slug,
-				Type:     getHitType(item),
-				ParentId: item.ParentId,
-				Tags:     []string{},
+				Id:          item.Id,
+				Title:       item.Title,
+				Uri:         "db/" + item.Slug,
+				Type:        getHitType(item),
+				FolderId:    item.FolderId,
+				FolderTitle: item.FolderTitle,
+				FolderSlug:  item.FolderSlug,
+				Tags:        []string{},
 			}
 			query.Result = append(query.Result, hit)
 			hits[item.Id] = hit
@@ -320,34 +295,6 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 	}
 }
 
-func convertToDashboardFolders(query *search.FindPersistedDashboardsQuery) error {
-	root := make(map[int64]*search.Hit)
-	var keys []int64
-
-	// Add dashboards and folders that should be at the root level
-	for _, item := range query.Result {
-		if item.Type == search.DashHitFolder || item.ParentId == 0 {
-			root[item.Id] = item
-			keys = append(keys, item.Id)
-		}
-	}
-
-	// Populate folders with their child dashboards
-	for _, item := range query.Result {
-		if item.Type == search.DashHitDB && item.ParentId > 0 {
-			root[item.ParentId].Dashboards = append(root[item.ParentId].Dashboards, *item)
-		}
-	}
-
-	query.Result = make([]*search.Hit, 0)
-
-	for _, key := range keys {
-		query.Result = append(query.Result, root[key])
-	}
-
-	return nil
-}
-
 func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
 	sql := `SELECT
 					  COUNT(*) as count,
@@ -379,7 +326,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
 			"DELETE FROM dashboard WHERE id = ?",
 			"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
 			"DELETE FROM dashboard_version WHERE dashboard_id = ?",
-			"DELETE FROM dashboard WHERE parent_id = ?",
+			"DELETE FROM dashboard WHERE folder_id = ?",
 		}
 
 		for _, sql := range deletes {

+ 3 - 3
pkg/services/sqlstore/dashboard_acl.go

@@ -40,7 +40,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
 
 		// Update dashboard HasAcl flag
 		dashboard := m.Dashboard{HasAcl: true}
-		if _, err := sess.Cols("has_acl").Where("id=? OR parent_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
+		if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
 			return err
 		}
 		return nil
@@ -105,7 +105,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
 			HasAcl: true,
 		}
 
-		if _, err := sess.Cols("has_acl").Where("id=? OR parent_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
+		if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil {
 			return err
 		}
 
@@ -129,7 +129,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 	dashboardFilter := fmt.Sprintf(`IN (
     SELECT %d
     UNION
-    SELECT parent_id from dashboard where id = %d
+    SELECT folder_id from dashboard where id = %d
   )`, query.DashboardId, query.DashboardId)
 
 	rawSQL := `

+ 16 - 37
pkg/services/sqlstore/dashboard_test.go

@@ -11,10 +11,10 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-func insertTestDashboard(title string, orgId int64, parentId int64, isFolder bool, tags ...interface{}) *m.Dashboard {
+func insertTestDashboard(title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *m.Dashboard {
 	cmd := m.SaveDashboardCommand{
 		OrgId:    orgId,
-		ParentId: parentId,
+		FolderId: folderId,
 		IsFolder: isFolder,
 		Dashboard: simplejson.NewFromAny(map[string]interface{}{
 			"id":    nil,
@@ -45,13 +45,13 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(savedDash.Slug, ShouldEqual, "test-dash-23")
 				So(savedDash.Id, ShouldNotEqual, 0)
 				So(savedDash.IsFolder, ShouldBeFalse)
-				So(savedDash.ParentId, ShouldBeGreaterThan, 0)
+				So(savedDash.FolderId, ShouldBeGreaterThan, 0)
 
 				So(savedFolder.Title, ShouldEqual, "1 test dash folder")
 				So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
 				So(savedFolder.Id, ShouldNotEqual, 0)
 				So(savedFolder.IsFolder, ShouldBeTrue)
-				So(savedFolder.ParentId, ShouldEqual, 0)
+				So(savedFolder.FolderId, ShouldEqual, 0)
 			})
 
 			Convey("Should be able to get dashboard", func() {
@@ -112,26 +112,6 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(err, ShouldNotBeNil)
 			})
 
-			Convey("Should be able to search for dashboard and return in folder hierarchy", func() {
-				query := search.FindPersistedDashboardsQuery{
-					Title:        "test dash 23",
-					OrgId:        1,
-					Mode:         "tree",
-					SignedInUser: &m.SignedInUser{OrgId: 1},
-				}
-
-				err := SearchDashboards(&query)
-				So(err, ShouldBeNil)
-
-				So(len(query.Result), ShouldEqual, 1)
-				hit := query.Result[0].Dashboards[0]
-
-				So(len(hit.Tags), ShouldEqual, 2)
-				So(hit.Type, ShouldEqual, search.DashHitDB)
-				So(hit.ParentId, ShouldBeGreaterThan, 0)
-
-			})
-
 			Convey("Should be able to search for dashboard folder", func() {
 				query := search.FindPersistedDashboardsQuery{
 					Title:        "1 test dash folder",
@@ -150,7 +130,7 @@ func TestDashboardDataAccess(t *testing.T) {
 			Convey("Should be able to search for a dashboard folder's children", func() {
 				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
-					ParentId:     savedFolder.Id,
+					FolderId:     savedFolder.Id,
 					SignedInUser: &m.SignedInUser{OrgId: 1},
 				}
 
@@ -166,19 +146,18 @@ func TestDashboardDataAccess(t *testing.T) {
 				Convey("should be able to find two dashboards by id", func() {
 					query := search.FindPersistedDashboardsQuery{
 						DashboardIds: []int64{2, 3},
-						Mode:         "tree",
 						SignedInUser: &m.SignedInUser{OrgId: 1},
 					}
 
 					err := SearchDashboards(&query)
 					So(err, ShouldBeNil)
 
-					So(len(query.Result[0].Dashboards), ShouldEqual, 2)
+					So(len(query.Result), ShouldEqual, 2)
 
-					hit := query.Result[0].Dashboards[0]
+					hit := query.Result[0]
 					So(len(hit.Tags), ShouldEqual, 2)
 
-					hit2 := query.Result[0].Dashboards[1]
+					hit2 := query.Result[1]
 					So(len(hit2.Tags), ShouldEqual, 1)
 				})
 
@@ -208,30 +187,30 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(err, ShouldNotBeNil)
 			})
 
-			Convey("Should be able to update dashboard and remove parentId", func() {
+			Convey("Should be able to update dashboard and remove folderId", func() {
 				cmd := m.SaveDashboardCommand{
 					OrgId: 1,
 					Dashboard: simplejson.NewFromAny(map[string]interface{}{
 						"id":    1,
-						"title": "parentId",
+						"title": "folderId",
 						"tags":  []interface{}{},
 					}),
 					Overwrite: true,
-					ParentId:  2,
+					FolderId:  2,
 				}
 
 				err := SaveDashboard(&cmd)
 				So(err, ShouldBeNil)
-				So(cmd.Result.ParentId, ShouldEqual, 2)
+				So(cmd.Result.FolderId, ShouldEqual, 2)
 
 				cmd = m.SaveDashboardCommand{
 					OrgId: 1,
 					Dashboard: simplejson.NewFromAny(map[string]interface{}{
 						"id":    1,
-						"title": "parentId",
+						"title": "folderId",
 						"tags":  []interface{}{},
 					}),
-					ParentId:  0,
+					FolderId:  0,
 					Overwrite: true,
 				}
 
@@ -245,7 +224,7 @@ func TestDashboardDataAccess(t *testing.T) {
 
 				err = GetDashboard(&query)
 				So(err, ShouldBeNil)
-				So(query.Result.ParentId, ShouldEqual, 0)
+				So(query.Result.FolderId, ShouldEqual, 0)
 			})
 
 			Convey("Should be able to delete a dashboard folder and its children", func() {
@@ -255,7 +234,7 @@ func TestDashboardDataAccess(t *testing.T) {
 
 				query := search.FindPersistedDashboardsQuery{
 					OrgId:        1,
-					ParentId:     savedFolder.Id,
+					FolderId:     savedFolder.Id,
 					SignedInUser: &m.SignedInUser{},
 				}
 

+ 3 - 3
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -137,9 +137,9 @@ func addDashboardMigration(mg *Migrator) {
 		{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
 	}))
 
-	// add column to store parent_id for dashboard folder structure
-	mg.AddMigration("Add column parent_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
-		Name: "parent_id", Type: DB_BigInt, Nullable: true,
+	// add column to store folder_id for dashboard folder structure
+	mg.AddMigration("Add column folder_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
+		Name: "folder_id", Type: DB_BigInt, Nullable: true,
 	}))
 
 	mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{

+ 3 - 18
public/app/core/components/search/search.html

@@ -53,8 +53,8 @@
 		<div class="search-results-container" ng-if="!ctrl.tagsMode">
 			<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
 
-    <div bindonce ng-repeat="row in ctrl.results">
-      <a class="search-item pointer search-item-{{row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
+    <div ng-repeat="row in ctrl.results">
+      <a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
         <span class="search-result-tags">
           <span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag"  class="label label-tag">
             {{tag}}
@@ -64,23 +64,8 @@
 
         <span class="search-result-link">
           <i class="fa search-result-icon"></i>
-          <span bo-text="row.title"></span>
+          {{::row.title}}
         </span>
-
-        <a class="search-item search-item-child pointer search-item-{{child.type}}" ng-repeat="child in row.dashboards"
-          ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{'dashboard/' + child.uri}}">
-          <span class="search-result-tags">
-            <span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in child.tags" tag-color-from-name="tag"  class="label label-tag">
-              {{tag}}
-            </span>
-            <i class="fa" ng-class="{'fa-star': child.isStarred, 'fa-star-o': !child.isStarred}"></i>
-          </span>
-
-          <span class="search-result-link">
-            <i class="fa search-result-icon"></i>
-            <span bo-text="child.title"></span>
-          </span>
-        </a>
       </a>
     </div>
 	</div>

+ 38 - 6
public/app/core/components/search/search.ts

@@ -106,17 +106,49 @@ export class SearchCtrl {
     this.currentSearchId = this.currentSearchId + 1;
     var localSearchId = this.currentSearchId;
 
-    return this.backendSrv.search(this.query).then((results) => {
+    return this.backendSrv.search(this.query).then(results => {
       if (localSearchId < this.currentSearchId) { return; }
 
-      this.results = _.map(results, function(dash) {
-        dash.url = 'dashboard/' + dash.uri;
-        return dash;
+      let byId = _.groupBy(results, 'id');
+      let byFolderId = _.groupBy(results, 'folderId');
+      let finalList = [];
+
+      // add missing parent folders
+      _.each(results, (hit, index) => {
+        if (hit.folderId && !byId[hit.folderId]) {
+          const folder = {
+            id: hit.folderId,
+            uri: `db/${hit.folderSlug}`,
+            title: hit.folderTitle,
+            type: 'dash-folder'
+          };
+          byId[hit.folderId] = folder;
+          results.splice(index, 0, folder);
+        }
       });
 
-      if (this.queryHasNoFilters()) {
-        this.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
+      // group by folder
+      for (let hit of results) {
+        if (hit.folderId) {
+          hit.type = "dash-child";
+        } else {
+          finalList.push(hit);
+        }
+
+        hit.url = 'dashboard/' + hit.uri;
+
+        if (hit.type === 'dash-folder') {
+          if (!byFolderId[hit.id]) {
+            continue;
+          }
+
+          for (let child of byFolderId[hit.id]) {
+            finalList.push(child);
+          }
+        }
       }
+
+      this.results = finalList;
     });
   }
 

+ 1 - 1
public/app/core/services/backend_srv.ts

@@ -211,7 +211,7 @@ export class BackendSrv {
 
     return this.post('/api/dashboards/db/', {
       dashboard: dash,
-      parentId: dash.parentId,
+      folderId: dash.folderId,
       overwrite: options.overwrite === true,
       message: options.message || '',
     });

+ 1 - 1
public/app/features/dashboard/dashboard_ctrl.ts

@@ -129,7 +129,7 @@ export class DashboardCtrl {
       };
 
       $scope.onFolderChange = function(folder) {
-        $scope.dashboard.parentId = folder.id;
+        $scope.dashboard.folderId = folder.id;
         $scope.dashboard.meta.folderId = folder.id;
         $scope.dashboard.meta.folderTitle= folder.title;
       };

+ 2 - 2
public/app/features/dashboard/dashnav/dashnav.ts

@@ -140,8 +140,8 @@ export class DashNavCtrl {
       var newWindow = window.open(uri);
     }
 
-    onFolderChange(parentId) {
-      this.dashboard.parentId = parentId;
+    onFolderChange(folderId) {
+      this.dashboard.folderId = folderId;
     }
 }
 

+ 2 - 2
public/app/features/dashboard/model.ts

@@ -36,7 +36,7 @@ export class DashboardModel {
   meta: any;
   events: any;
   editMode: boolean;
-  parentId: number;
+  folderId: number;
 
   constructor(data, meta?) {
     if (!data) {
@@ -65,7 +65,7 @@ export class DashboardModel {
     this.version = data.version || 0;
     this.links = data.links || [];
     this.gnetId = data.gnetId || null;
-    this.parentId = data.parentId || null;
+    this.folderId = data.folderId || null;
 
     this.rows = [];
     if (data.rows) {

+ 1 - 1
public/app/features/dashboard/save_as_modal.ts

@@ -73,7 +73,7 @@ export class SaveDashboardAsModalCtrl {
   }
 
   onFolderChange(folder) {
-    this.clone.parentId = folder.id;
+    this.clone.folderId = folder.id;
   }
 }
 

+ 27 - 77
public/sass/components/_search.scss

@@ -104,95 +104,45 @@
       padding-right: 10px;
     }
   }
+}
 
-  .search-item {
-    word-wrap: break-word;
-    display: block;
-    padding: 3px 10px;
-    white-space: nowrap;
-    background-color: $tight-form-bg;
-    margin-bottom: 4px;
-    @include left-brand-border();
-
-    &:hover,
-    &.selected {
-      background-color: $tight-form-func-bg;
-      @include left-brand-border-gradient();
-    }
-  }
+.search-item {
+  word-wrap: break-word;
+  display: block;
+  padding: 3px 10px;
+  white-space: nowrap;
+  background-color: $tight-form-bg;
+  margin-bottom: 4px;
+  @include left-brand-border();
 
-  .search-result-tags {
-    float: right;
+  &:hover {
+    @include left-brand-border-gradient();
+    background-color: $tight-form-func-bg;
   }
 
-  .search-result-actions {
-    float: right;
-    padding-left: 20px;
+  &.selected {
+    background-color: $tight-form-func-bg;
   }
-}
-
-.search-result-icon::before {
-  content: "\f009";
-}
-
-.search-item-dash-home .search-result-icon::before {
-  content: "\f015";
-}
-
-.search-item-dash-home .search-result-icon::before {
-  content: "\f015";
-}
-
-.search-item-child {
-  margin-left: 20px;
-}
-
-.search-item-dash-home > .search-result-link > .search-result-icon::before {
-  content: "\f015";
-}
-
-.search-item-dash-folder > .search-result-link > .search-result-icon::before {
-  content: "\f07c";
-}
-
-.search-button-row {
-  padding: $spacer*2;
-  display: flex;
-  flex-direction: row;
-  align-items: flex-start;
-  justify-content: space-around;
-  height: 30%;
 
-  button, a {
-    margin-bottom: $spacer;
+  &--dash-db,
+  &--dash-child {
+    .search-result-icon::before {
+      content: "\f009";
+    }
   }
 
-  .search-button-row-explore-link {
-    color: $gray-3;
-    font-size: $font-size-sm;
-    position: relative;
-    top: 1.0rem;
-    &:hover {
-      color: $link-hover-color;
-    }
-    img {
-      vertical-align: text-bottom;
+  &--dash-folder {
+    .search-result-icon::before {
+      content: "\f07c";
     }
   }
-}
 
-@include media-breakpoint-up(lg) {
-  .search-dropdown {
-    flex-direction: row;
-  }
-  .search-button-row {
-    flex-direction: column;
-    justify-content: flex-start;
+  &--dash-child {
+    margin-left: 20px;
   }
 }
 
-@include media-breakpoint-up(md) {
-  .search-container {
-    left: 78px;
-  }
+.search-result-tags {
+  float: right;
 }
+