Ver Fonte

WIP: rough prototype of dashboard folders

Breaks some stuff like selected dash in the search result.
In dashboard search, if the user is not searching then the result is
returned as a tree structure. No ACL's or user group ux yet.
Daniel Lee há 8 anos atrás
pai
commit
1248728d7f

+ 2 - 0
pkg/api/search.go

@@ -14,6 +14,7 @@ func Search(c *middleware.Context) {
 	tags := c.QueryStrings("tag")
 	starred := c.Query("starred")
 	limit := c.QueryInt("limit")
+	browseMode := c.Query("browseMode")
 
 	if limit == 0 {
 		limit = 1000
@@ -35,6 +36,7 @@ func Search(c *middleware.Context) {
 		IsStarred:    starred == "true",
 		OrgId:        c.OrgId,
 		DashboardIds: dbids,
+		BrowseMode:   browseMode == "true",
 	}
 
 	err := bus.Dispatch(&searchQuery)

+ 14 - 0
pkg/models/dashboards.go

@@ -18,6 +18,14 @@ var (
 	ErrDashboardTitleEmpty         = errors.New("Dashboard title cannot be empty")
 )
 
+type PermissionType int
+
+const (
+	PERMISSION_EDIT           PermissionType = 4
+	PERMISSION_READ_ONLY_EDIT PermissionType = 2
+	PERMISSION_VIEW           PermissionType = 1
+)
+
 type UpdatePluginDashboardError struct {
 	PluginId string
 }
@@ -47,6 +55,8 @@ type Dashboard struct {
 
 	UpdatedBy int64
 	CreatedBy int64
+	ParentId  int64
+	IsFolder  bool
 
 	Title string
 	Data  *simplejson.Json
@@ -111,6 +121,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 	dash.UpdatedBy = userId
 	dash.OrgId = cmd.OrgId
 	dash.PluginId = cmd.PluginId
+	dash.IsFolder = cmd.IsFolder
+	dash.ParentId = cmd.ParentId
 	dash.UpdateSlug()
 	return dash
 }
@@ -138,6 +150,8 @@ type SaveDashboardCommand struct {
 	OrgId        int64            `json:"-"`
 	RestoredFrom int              `json:"-"`
 	PluginId     string           `json:"-"`
+	ParentId  int64               `json:"parentId"`
+	IsFolder  bool                `json:"isFolder"`
 
 	Result *Dashboard
 }

+ 13 - 0
pkg/models/user_group.go

@@ -0,0 +1,13 @@
+package models
+
+import "time"
+
+// UserGroup model
+type UserGroup struct {
+	Id    int64
+	OrgId int64
+	Name  string
+
+	Created time.Time
+	Updated time.Time
+}

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

@@ -44,6 +44,7 @@ func searchHandler(query *Query) error {
 		IsStarred:    query.IsStarred,
 		OrgId:        query.OrgId,
 		DashboardIds: query.DashboardIds,
+		BrowseMode:   query.BrowseMode,
 	}
 
 	if err := bus.Dispatch(&dashQuery); err != nil {

+ 15 - 0
pkg/services/search/handlers_test.go

@@ -19,6 +19,9 @@ func TestSearch(t *testing.T) {
 				&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}},
 				&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}},
 				&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}},
+				&Hit{Id: 17, Title: "FOLDER", Dashboards: []Hit{
+					{Id: 18, Title: "ZZAA", Tags: []string{"ZZ"}},
+				}},
 			}
 			return nil
 		})
@@ -57,5 +60,17 @@ func TestSearch(t *testing.T) {
 			})
 
 		})
+
+		Convey("That returns result in browse mode", func() {
+			query.BrowseMode = true
+			err := searchHandler(&query)
+			So(err, ShouldBeNil)
+
+			Convey("should return correct results", func() {
+				So(query.Result[3].Title, ShouldEqual, "FOLDER")
+				So(len(query.Result[3].Dashboards), ShouldEqual, 1)
+			})
+
+		})
 	})
 }

+ 11 - 6
pkg/services/search/models.go

@@ -7,15 +7,18 @@ const (
 	DashHitHome     HitType = "dash-home"
 	DashHitJson     HitType = "dash-json"
 	DashHitScripted HitType = "dash-scripted"
+	DashHitFolder   HitType = "dash-folder"
 )
 
 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"`
+	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"`
 }
 
 type HitList []*Hit
@@ -32,6 +35,7 @@ type Query struct {
 	Limit        int
 	IsStarred    bool
 	DashboardIds []int
+	BrowseMode   bool
 
 	Result HitList
 }
@@ -42,6 +46,7 @@ type FindPersistedDashboardsQuery struct {
 	UserId       int64
 	IsStarred    bool
 	DashboardIds []int
+	BrowseMode   bool
 
 	Result HitList
 }

+ 1 - 1
pkg/services/sqlstore/alert_test.go

@@ -12,7 +12,7 @@ func TestAlertingDataAccess(t *testing.T) {
 	Convey("Testing Alerting data access", t, func() {
 		InitTestDB(t)
 
-		testDash := insertTestDashboard("dashboard with alerts", 1, "alert")
+		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
 
 		items := []*m.Alert{
 			{

+ 68 - 12
pkg/services/sqlstore/dashboard.go

@@ -148,13 +148,15 @@ func GetDashboard(query *m.GetDashboardQuery) error {
 }
 
 type DashboardSearchProjection struct {
-	Id    int64
-	Title string
-	Slug  string
-	Term  string
+	Id       int64
+	Title    string
+	Slug     string
+	Term     string
+	IsFolder bool
+	ParentId int64
 }
 
-func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
+func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
 	var sql bytes.Buffer
 	params := make([]interface{}, 0)
 
@@ -162,7 +164,9 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 					  dashboard.id,
 					  dashboard.title,
 					  dashboard.slug,
-					  dashboard_tag.term
+					  dashboard_tag.term,
+            dashboard.is_folder,
+            dashboard.parent_id
 					FROM dashboard
 					LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
 
@@ -200,8 +204,16 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 	sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
 
 	var res []DashboardSearchProjection
-
 	err := x.Sql(sql.String(), params...).Find(&res)
+	if err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}
+
+func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
+	res, err := findDashboards(query)
 	if err != nil {
 		return err
 	}
@@ -213,11 +225,12 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 		hit, exists := hits[item.Id]
 		if !exists {
 			hit = &search.Hit{
-				Id:    item.Id,
-				Title: item.Title,
-				Uri:   "db/" + item.Slug,
-				Type:  search.DashHitDB,
-				Tags:  []string{},
+				Id:       item.Id,
+				Title:    item.Title,
+				Uri:      "db/" + item.Slug,
+				Type:     getHitType(item),
+				ParentId: item.ParentId,
+				Tags:     []string{},
 			}
 			query.Result = append(query.Result, hit)
 			hits[item.Id] = hit
@@ -227,9 +240,52 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
 		}
 	}
 
+	if query.BrowseMode {
+		convertToDashboardFolders(query)
+	}
+
 	return err
 }
 
+func getHitType(item DashboardSearchProjection) search.HitType {
+	var hitType search.HitType
+	if item.IsFolder {
+		hitType = search.DashHitFolder
+	} else {
+		hitType = search.DashHitDB
+	}
+
+	return hitType
+}
+
+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,

+ 53 - 8
pkg/services/sqlstore/dashboard_test.go

@@ -11,9 +11,11 @@ import (
 	"github.com/grafana/grafana/pkg/services/search"
 )
 
-func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
+func insertTestDashboard(title string, orgId int64, parentId int64, isFolder bool, tags ...interface{}) *m.Dashboard {
 	cmd := m.SaveDashboardCommand{
-		OrgId: orgId,
+		OrgId:    orgId,
+		ParentId: parentId,
+		IsFolder: isFolder,
 		Dashboard: simplejson.NewFromAny(map[string]interface{}{
 			"id":    nil,
 			"title": title,
@@ -33,14 +35,23 @@ func TestDashboardDataAccess(t *testing.T) {
 		InitTestDB(t)
 
 		Convey("Given saved dashboard", func() {
-			savedDash := insertTestDashboard("test dash 23", 1, "prod", "webapp")
-			insertTestDashboard("test dash 45", 1, "prod")
-			insertTestDashboard("test dash 67", 1, "prod", "webapp")
+			savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
+			savedDash := insertTestDashboard("test dash 23", 1, savedFolder.Id, false, "prod", "webapp")
+			insertTestDashboard("test dash 45", 1, savedFolder.Id, false, "prod")
+			insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
 
 			Convey("Should return dashboard model", func() {
 				So(savedDash.Title, ShouldEqual, "test dash 23")
 				So(savedDash.Slug, ShouldEqual, "test-dash-23")
 				So(savedDash.Id, ShouldNotEqual, 0)
+				So(savedDash.IsFolder, ShouldBeFalse)
+				So(savedDash.ParentId, 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)
 			})
 
 			Convey("Should be able to get dashboard", func() {
@@ -54,10 +65,11 @@ func TestDashboardDataAccess(t *testing.T) {
 
 				So(query.Result.Title, ShouldEqual, "test dash 23")
 				So(query.Result.Slug, ShouldEqual, "test-dash-23")
+				So(query.Result.IsFolder, ShouldBeFalse)
 			})
 
 			Convey("Should be able to delete dashboard", func() {
-				insertTestDashboard("delete me", 1, "delete this")
+				insertTestDashboard("delete me", 1, 0, false, "delete this")
 
 				dashboardSlug := slug.Make("delete me")
 
@@ -114,12 +126,45 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(len(query.Result), ShouldEqual, 1)
 				hit := query.Result[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",
+					OrgId: 1,
+				}
+
+				err := SearchDashboards(&query)
+				So(err, ShouldBeNil)
+
+				So(len(query.Result), ShouldEqual, 1)
+				hit := query.Result[0]
+				So(hit.Type, ShouldEqual, search.DashHitFolder)
+			})
+
+			Convey("Should be able to browse dashboard folders", func() {
+				query := search.FindPersistedDashboardsQuery{
+					OrgId:      1,
+					BrowseMode: true,
+				}
+
+				err := SearchDashboards(&query)
+				So(err, ShouldBeNil)
+
+				So(len(query.Result), ShouldEqual, 2)
+				hit := query.Result[0]
+				So(hit.Type, ShouldEqual, search.DashHitFolder)
+				So(len(hit.Dashboards), ShouldEqual, 2)
+				So(hit.Dashboards[0].Title, ShouldEqual, "test dash 23")
+
 			})
 
 			Convey("Should be able to search for dashboard by dashboard ids", func() {
 				Convey("should be able to find two dashboards by id", func() {
 					query := search.FindPersistedDashboardsQuery{
-						DashboardIds: []int{1, 2},
+						DashboardIds: []int{2, 3},
 						OrgId:        1,
 					}
 
@@ -171,7 +216,7 @@ func TestDashboardDataAccess(t *testing.T) {
 			})
 
 			Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
-				starredDash := insertTestDashboard("starred dash", 1)
+				starredDash := insertTestDashboard("starred dash", 1, 0, false)
 				StarDashboard(&m.StarDashboardCommand{
 					DashboardId: starredDash.Id,
 					UserId:      10,

+ 36 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -136,4 +136,40 @@ func addDashboardMigration(mg *Migrator) {
 	mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{
 		{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,
+	}))
+
+	mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{
+		Name: "is_folder", Type: DB_Bool, Nullable: false, Default: "0",
+	}))
+
+	dashboardAclV1 := Table{
+		Name: "dashboard_acl",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "org_id", Type: DB_BigInt},
+			{Name: "dashboard_id", Type: DB_BigInt},
+			{Name: "user_id", Type: DB_BigInt, Nullable: true},
+			{Name: "user_group_id", Type: DB_BigInt, Nullable: true},
+			{Name: "permissions", Type: DB_SmallInt, Default: "4"},
+			{Name: "created", Type: DB_DateTime, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"org_id"}},
+			{Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex},
+			{Cols: []string{"dashboard_id", "user_group_id"}, Type: UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("create dashboard  acl table", NewAddTableMigration(dashboardAclV1))
+
+	//-------  indexes ------------------
+	mg.AddMigration("add unique index dashboard_acl_org_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0]))
+	mg.AddMigration("add unique index dashboard_acl_dashboard_id_user_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[1]))
+	mg.AddMigration("add unique index dashboard_acl_dashboard_id_group_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[2]))
+
 }

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -26,6 +26,7 @@ func AddMigrations(mg *Migrator) {
 	addAnnotationMig(mg)
 	addTestDataMigrations(mg)
 	addDashboardVersionMigration(mg)
+	addUserGroupMigrations(mg)
 }
 
 func addMigrationLogMigrations(mg *Migrator) {

+ 48 - 0
pkg/services/sqlstore/migrations/user_group_mig.go

@@ -0,0 +1,48 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addUserGroupMigrations(mg *Migrator) {
+	userGroupV1 := Table{
+		Name: "user_group",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "org_id", Type: DB_BigInt},
+			{Name: "created", Type: DB_DateTime, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"org_id"}},
+			{Cols: []string{"org_id", "name"}, Type: UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("create user group table", NewAddTableMigration(userGroupV1))
+
+	//-------  indexes ------------------
+	mg.AddMigration("add index user_group.org_id", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[0]))
+	mg.AddMigration("add unique index user_group_org_id_name", NewAddIndexMigration(userGroupV1, userGroupV1.Indices[1]))
+
+	userGroupMemberV1 := Table{
+		Name: "user_group_member",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "org_id", Type: DB_BigInt},
+			{Name: "user_group_id", Type: DB_BigInt},
+			{Name: "user_id", Type: DB_BigInt},
+			{Name: "created", Type: DB_DateTime, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"org_id"}},
+			{Cols: []string{"org_id", "user_group_id", "user_id"}, Type: UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("create user group member table", NewAddTableMigration(userGroupMemberV1))
+
+	//-------  indexes ------------------
+	mg.AddMigration("add index user_group_member.org_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[0]))
+	mg.AddMigration("add unique index user_group_member_org_id_user_group_id_user_id", NewAddIndexMigration(userGroupMemberV1, userGroupMemberV1.Indices[1]))
+}

+ 31 - 15
public/app/core/components/search/search.html

@@ -56,22 +56,38 @@
 		<div class="search-results-container" ng-if="!ctrl.tagsMode">
 			<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
 
-			<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
-				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}}
-					</span>
-					<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
-				</span>
+    <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}}">
 
-				<span class="search-result-link">
-					<i class="fa search-result-icon"></i>
-					<span bo-text="row.title"></span>
-				</span>
-			</a>
-		</div>
+        <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}}
+          </span>
+          <i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
+        </span>
+
+        <span class="search-result-link">
+          <i class="fa search-result-icon"></i>
+          <span bo-text="row.title"></span>
+        </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>
 
 		<div class="search-button-row">
 			<a class="btn btn-secondary" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">

+ 2 - 0
public/app/core/components/search/search.ts

@@ -104,6 +104,8 @@ export class SearchCtrl {
     this.currentSearchId = this.currentSearchId + 1;
     var localSearchId = this.currentSearchId;
 
+    this.query.browseMode = this.queryHasNoFilters();
+
     return this.backendSrv.search(this.query).then((results) => {
       if (localSearchId < this.currentSearchId) { return; }
 

+ 11 - 4
public/sass/components/_search.scss

@@ -118,10 +118,6 @@
       content: "\f009";
     }
 
-    &.search-item-dash-home .search-result-icon::before {
-      content: "\f015";
-    }
-
     &:hover {
       background-color: $tight-form-func-bg;
       @include left-brand-border-gradient();
@@ -142,6 +138,17 @@
   }
 }
 
+.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;