mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
dashboard folder search fix
This commit is contained in:
@@ -88,13 +88,13 @@ func GetDashboard(c *middleware.Context) Response {
|
|||||||
Version: dash.Version,
|
Version: dash.Version,
|
||||||
HasAcl: dash.HasAcl,
|
HasAcl: dash.HasAcl,
|
||||||
IsFolder: dash.IsFolder,
|
IsFolder: dash.IsFolder,
|
||||||
FolderId: dash.ParentId,
|
FolderId: dash.FolderId,
|
||||||
FolderTitle: "Root",
|
FolderTitle: "Root",
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup folder title
|
// lookup folder title
|
||||||
if dash.ParentId > 0 {
|
if dash.FolderId > 0 {
|
||||||
query := m.GetDashboardQuery{Id: dash.ParentId, OrgId: c.OrgId}
|
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return ApiError(500, "Dashboard folder could not be read", err)
|
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)
|
return dashboardGuardianResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dash.IsFolder && dash.ParentId > 0 {
|
if dash.IsFolder && dash.FolderId > 0 {
|
||||||
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
|
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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() {
|
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
|
||||||
fakeDash := m.NewDashboard("Child dash")
|
fakeDash := m.NewDashboard("Child dash")
|
||||||
fakeDash.Id = 1
|
fakeDash.Id = 1
|
||||||
fakeDash.ParentId = 1
|
fakeDash.FolderId = 1
|
||||||
fakeDash.HasAcl = false
|
fakeDash.HasAcl = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||||
@@ -50,7 +50,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
cmd := m.SaveDashboardCommand{
|
cmd := m.SaveDashboardCommand{
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"parentId": fakeDash.ParentId,
|
"folderId": fakeDash.FolderId,
|
||||||
"title": fakeDash.Title,
|
"title": fakeDash.Title,
|
||||||
"id": fakeDash.Id,
|
"id": fakeDash.Id,
|
||||||
}),
|
}),
|
||||||
@@ -163,10 +163,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
invalidCmd := m.SaveDashboardCommand{
|
invalidCmd := m.SaveDashboardCommand{
|
||||||
ParentId: fakeDash.ParentId,
|
FolderId: fakeDash.FolderId,
|
||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"parentId": fakeDash.ParentId,
|
"folderId": fakeDash.FolderId,
|
||||||
"title": fakeDash.Title,
|
"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() {
|
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
|
||||||
fakeDash := m.NewDashboard("Child dash")
|
fakeDash := m.NewDashboard("Child dash")
|
||||||
fakeDash.Id = 1
|
fakeDash.Id = 1
|
||||||
fakeDash.ParentId = 1
|
fakeDash.FolderId = 1
|
||||||
fakeDash.HasAcl = true
|
fakeDash.HasAcl = true
|
||||||
|
|
||||||
aclMockResp := []*m.DashboardAclInfoDTO{
|
aclMockResp := []*m.DashboardAclInfoDTO{
|
||||||
@@ -210,10 +210,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd := m.SaveDashboardCommand{
|
cmd := m.SaveDashboardCommand{
|
||||||
ParentId: fakeDash.ParentId,
|
FolderId: fakeDash.FolderId,
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"id": fakeDash.Id,
|
"id": fakeDash.Id,
|
||||||
"parentId": fakeDash.ParentId,
|
"folderId": fakeDash.FolderId,
|
||||||
"title": fakeDash.Title,
|
"title": fakeDash.Title,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ type Dashboard struct {
|
|||||||
|
|
||||||
UpdatedBy int64
|
UpdatedBy int64
|
||||||
CreatedBy int64
|
CreatedBy int64
|
||||||
ParentId int64
|
FolderId int64
|
||||||
IsFolder bool
|
IsFolder bool
|
||||||
HasAcl bool
|
HasAcl bool
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
|||||||
dash.OrgId = cmd.OrgId
|
dash.OrgId = cmd.OrgId
|
||||||
dash.PluginId = cmd.PluginId
|
dash.PluginId = cmd.PluginId
|
||||||
dash.IsFolder = cmd.IsFolder
|
dash.IsFolder = cmd.IsFolder
|
||||||
dash.ParentId = cmd.ParentId
|
dash.FolderId = cmd.FolderId
|
||||||
dash.UpdateSlug()
|
dash.UpdateSlug()
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ type SaveDashboardCommand struct {
|
|||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
RestoredFrom int `json:"-"`
|
RestoredFrom int `json:"-"`
|
||||||
PluginId string `json:"-"`
|
PluginId string `json:"-"`
|
||||||
ParentId int64 `json:"parentId"`
|
FolderId int64 `json:"folderId"`
|
||||||
IsFolder bool `json:"isFolder"`
|
IsFolder bool `json:"isFolder"`
|
||||||
|
|
||||||
Result *Dashboard
|
Result *Dashboard
|
||||||
|
@@ -44,11 +44,11 @@ func TestDashboardModel(t *testing.T) {
|
|||||||
json := simplejson.New()
|
json := simplejson.New()
|
||||||
json.Set("title", "test dash")
|
json.Set("title", "test dash")
|
||||||
|
|
||||||
cmd := &SaveDashboardCommand{Dashboard: json, ParentId: 1}
|
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
|
||||||
dash := cmd.GetDashboardModel()
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
Convey("Should set ParentId", func() {
|
Convey("Should set FolderId", func() {
|
||||||
So(dash.ParentId, ShouldEqual, 1)
|
So(dash.FolderId, ShouldEqual, 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ func searchHandler(query *Query) error {
|
|||||||
IsStarred: query.IsStarred,
|
IsStarred: query.IsStarred,
|
||||||
DashboardIds: query.DashboardIds,
|
DashboardIds: query.DashboardIds,
|
||||||
Type: query.Type,
|
Type: query.Type,
|
||||||
ParentId: query.FolderId,
|
FolderId: query.FolderId,
|
||||||
Mode: query.Mode,
|
Mode: query.Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,9 +20,7 @@ func TestSearch(t *testing.T) {
|
|||||||
&Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}},
|
&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: 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: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}},
|
||||||
&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder", Dashboards: []Hit{
|
&Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"},
|
||||||
{Id: 18, Title: "ZZAA", Tags: []string{"ZZ"}},
|
|
||||||
}},
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@@ -14,14 +14,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Hit struct {
|
type Hit struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Uri string `json:"uri"`
|
Uri string `json:"uri"`
|
||||||
Type HitType `json:"type"`
|
Type HitType `json:"type"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
IsStarred bool `json:"isStarred"`
|
IsStarred bool `json:"isStarred"`
|
||||||
ParentId int64 `json:"parentId"`
|
FolderId int64 `json:"folderId,omitempty"`
|
||||||
Dashboards []Hit `json:"dashboards"`
|
FolderTitle string `json:"folderTitle,omitempty"`
|
||||||
|
FolderSlug string `json:"folderSlug,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HitList []*Hit
|
type HitList []*Hit
|
||||||
@@ -62,7 +63,7 @@ type FindPersistedDashboardsQuery struct {
|
|||||||
IsStarred bool
|
IsStarred bool
|
||||||
DashboardIds []int64
|
DashboardIds []int64
|
||||||
Type string
|
Type string
|
||||||
ParentId int64
|
FolderId int64
|
||||||
Mode string
|
Mode string
|
||||||
|
|
||||||
Result HitList
|
Result HitList
|
||||||
|
@@ -81,7 +81,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
|||||||
} else {
|
} else {
|
||||||
dash.Version += 1
|
dash.Version += 1
|
||||||
dash.Data.Set("version", dash.Version)
|
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 {
|
if err != nil {
|
||||||
@@ -153,7 +153,7 @@ type DashboardSearchProjection struct {
|
|||||||
Slug string
|
Slug string
|
||||||
Term string
|
Term string
|
||||||
IsFolder bool
|
IsFolder bool
|
||||||
ParentId int64
|
FolderId int64
|
||||||
FolderSlug string
|
FolderSlug string
|
||||||
FolderTitle string
|
FolderTitle string
|
||||||
}
|
}
|
||||||
@@ -168,11 +168,11 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
|
|||||||
dashboard.slug,
|
dashboard.slug,
|
||||||
dashboard_tag.term,
|
dashboard_tag.term,
|
||||||
dashboard.is_folder,
|
dashboard.is_folder,
|
||||||
dashboard.parent_id,
|
dashboard.folder_id,
|
||||||
f.slug as folder_slug,
|
f.slug as folder_slug,
|
||||||
f.title as folder_title
|
f.title as folder_title
|
||||||
FROM dashboard
|
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`)
|
LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`)
|
||||||
|
|
||||||
if query.IsStarred {
|
if query.IsStarred {
|
||||||
@@ -204,7 +204,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
|
|||||||
allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
|
allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
|
||||||
SELECT distinct d.id AS DashboardId
|
SELECT distinct d.id AS DashboardId
|
||||||
FROM dashboard AS d
|
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 user_group_member as ugm on ugm.user_group_id = da.user_group_id
|
||||||
LEFT JOIN org_user ou on ou.role = da.role
|
LEFT JOIN org_user ou on ou.role = da.role
|
||||||
WHERE
|
WHERE
|
||||||
@@ -230,9 +230,9 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
|
|||||||
sql.WriteString(" AND dashboard.is_folder = 0")
|
sql.WriteString(" AND dashboard.is_folder = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.ParentId > 0 {
|
if query.FolderId > 0 {
|
||||||
sql.WriteString(" AND dashboard.parent_id = ?")
|
sql.WriteString(" AND dashboard.folder_id = ?")
|
||||||
params = append(params, query.ParentId)
|
params = append(params, query.FolderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
|
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
|
||||||
@@ -253,38 +253,11 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Mode == "tree" {
|
|
||||||
res, err = appendDashboardFolders(res)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
makeQueryResult(query, res)
|
makeQueryResult(query, res)
|
||||||
|
|
||||||
if query.Mode == "tree" {
|
|
||||||
convertToDashboardFolders(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
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 {
|
func getHitType(item DashboardSearchProjection) search.HitType {
|
||||||
var hitType search.HitType
|
var hitType search.HitType
|
||||||
if item.IsFolder {
|
if item.IsFolder {
|
||||||
@@ -304,12 +277,14 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
|
|||||||
hit, exists := hits[item.Id]
|
hit, exists := hits[item.Id]
|
||||||
if !exists {
|
if !exists {
|
||||||
hit = &search.Hit{
|
hit = &search.Hit{
|
||||||
Id: item.Id,
|
Id: item.Id,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Uri: "db/" + item.Slug,
|
Uri: "db/" + item.Slug,
|
||||||
Type: getHitType(item),
|
Type: getHitType(item),
|
||||||
ParentId: item.ParentId,
|
FolderId: item.FolderId,
|
||||||
Tags: []string{},
|
FolderTitle: item.FolderTitle,
|
||||||
|
FolderSlug: item.FolderSlug,
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
query.Result = append(query.Result, hit)
|
query.Result = append(query.Result, hit)
|
||||||
hits[item.Id] = 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 {
|
func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
|
||||||
sql := `SELECT
|
sql := `SELECT
|
||||||
COUNT(*) as count,
|
COUNT(*) as count,
|
||||||
@@ -379,7 +326,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
|||||||
"DELETE FROM dashboard WHERE id = ?",
|
"DELETE FROM dashboard WHERE id = ?",
|
||||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||||
"DELETE FROM dashboard WHERE parent_id = ?",
|
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sql := range deletes {
|
for _, sql := range deletes {
|
||||||
|
@@ -40,7 +40,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
|
|||||||
|
|
||||||
// Update dashboard HasAcl flag
|
// Update dashboard HasAcl flag
|
||||||
dashboard := m.Dashboard{HasAcl: true}
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -105,7 +105,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
|
|||||||
HasAcl: true,
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
|||||||
dashboardFilter := fmt.Sprintf(`IN (
|
dashboardFilter := fmt.Sprintf(`IN (
|
||||||
SELECT %d
|
SELECT %d
|
||||||
UNION
|
UNION
|
||||||
SELECT parent_id from dashboard where id = %d
|
SELECT folder_id from dashboard where id = %d
|
||||||
)`, query.DashboardId, query.DashboardId)
|
)`, query.DashboardId, query.DashboardId)
|
||||||
|
|
||||||
rawSQL := `
|
rawSQL := `
|
||||||
|
@@ -11,10 +11,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"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{
|
cmd := m.SaveDashboardCommand{
|
||||||
OrgId: orgId,
|
OrgId: orgId,
|
||||||
ParentId: parentId,
|
FolderId: folderId,
|
||||||
IsFolder: isFolder,
|
IsFolder: isFolder,
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"id": nil,
|
"id": nil,
|
||||||
@@ -45,13 +45,13 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
So(savedDash.Slug, ShouldEqual, "test-dash-23")
|
So(savedDash.Slug, ShouldEqual, "test-dash-23")
|
||||||
So(savedDash.Id, ShouldNotEqual, 0)
|
So(savedDash.Id, ShouldNotEqual, 0)
|
||||||
So(savedDash.IsFolder, ShouldBeFalse)
|
So(savedDash.IsFolder, ShouldBeFalse)
|
||||||
So(savedDash.ParentId, ShouldBeGreaterThan, 0)
|
So(savedDash.FolderId, ShouldBeGreaterThan, 0)
|
||||||
|
|
||||||
So(savedFolder.Title, ShouldEqual, "1 test dash folder")
|
So(savedFolder.Title, ShouldEqual, "1 test dash folder")
|
||||||
So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
|
So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
|
||||||
So(savedFolder.Id, ShouldNotEqual, 0)
|
So(savedFolder.Id, ShouldNotEqual, 0)
|
||||||
So(savedFolder.IsFolder, ShouldBeTrue)
|
So(savedFolder.IsFolder, ShouldBeTrue)
|
||||||
So(savedFolder.ParentId, ShouldEqual, 0)
|
So(savedFolder.FolderId, ShouldEqual, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to get dashboard", func() {
|
Convey("Should be able to get dashboard", func() {
|
||||||
@@ -112,26 +112,6 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
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() {
|
Convey("Should be able to search for dashboard folder", func() {
|
||||||
query := search.FindPersistedDashboardsQuery{
|
query := search.FindPersistedDashboardsQuery{
|
||||||
Title: "1 test dash folder",
|
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() {
|
Convey("Should be able to search for a dashboard folder's children", func() {
|
||||||
query := search.FindPersistedDashboardsQuery{
|
query := search.FindPersistedDashboardsQuery{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
ParentId: savedFolder.Id,
|
FolderId: savedFolder.Id,
|
||||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
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() {
|
Convey("should be able to find two dashboards by id", func() {
|
||||||
query := search.FindPersistedDashboardsQuery{
|
query := search.FindPersistedDashboardsQuery{
|
||||||
DashboardIds: []int64{2, 3},
|
DashboardIds: []int64{2, 3},
|
||||||
Mode: "tree",
|
|
||||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := SearchDashboards(&query)
|
err := SearchDashboards(&query)
|
||||||
So(err, ShouldBeNil)
|
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)
|
So(len(hit.Tags), ShouldEqual, 2)
|
||||||
|
|
||||||
hit2 := query.Result[0].Dashboards[1]
|
hit2 := query.Result[1]
|
||||||
So(len(hit2.Tags), ShouldEqual, 1)
|
So(len(hit2.Tags), ShouldEqual, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -208,30 +187,30 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
So(err, ShouldNotBeNil)
|
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{
|
cmd := m.SaveDashboardCommand{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "parentId",
|
"title": "folderId",
|
||||||
"tags": []interface{}{},
|
"tags": []interface{}{},
|
||||||
}),
|
}),
|
||||||
Overwrite: true,
|
Overwrite: true,
|
||||||
ParentId: 2,
|
FolderId: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := SaveDashboard(&cmd)
|
err := SaveDashboard(&cmd)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(cmd.Result.ParentId, ShouldEqual, 2)
|
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||||
|
|
||||||
cmd = m.SaveDashboardCommand{
|
cmd = m.SaveDashboardCommand{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "parentId",
|
"title": "folderId",
|
||||||
"tags": []interface{}{},
|
"tags": []interface{}{},
|
||||||
}),
|
}),
|
||||||
ParentId: 0,
|
FolderId: 0,
|
||||||
Overwrite: true,
|
Overwrite: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +224,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
|||||||
|
|
||||||
err = GetDashboard(&query)
|
err = GetDashboard(&query)
|
||||||
So(err, ShouldBeNil)
|
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() {
|
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{
|
query := search.FindPersistedDashboardsQuery{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
ParentId: savedFolder.Id,
|
FolderId: savedFolder.Id,
|
||||||
SignedInUser: &m.SignedInUser{},
|
SignedInUser: &m.SignedInUser{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -137,9 +137,9 @@ func addDashboardMigration(mg *Migrator) {
|
|||||||
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
|
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// add column to store parent_id for dashboard folder structure
|
// add column to store folder_id for dashboard folder structure
|
||||||
mg.AddMigration("Add column parent_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
mg.AddMigration("Add column folder_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||||
Name: "parent_id", Type: DB_BigInt, Nullable: true,
|
Name: "folder_id", Type: DB_BigInt, Nullable: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||||
|
@@ -53,8 +53,8 @@
|
|||||||
<div class="search-results-container" ng-if="!ctrl.tagsMode">
|
<div class="search-results-container" ng-if="!ctrl.tagsMode">
|
||||||
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
|
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
|
||||||
|
|
||||||
<div bindonce ng-repeat="row in ctrl.results">
|
<div 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}}">
|
<a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
|
||||||
<span class="search-result-tags">
|
<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">
|
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
|
||||||
{{tag}}
|
{{tag}}
|
||||||
@@ -64,23 +64,8 @@
|
|||||||
|
|
||||||
<span class="search-result-link">
|
<span class="search-result-link">
|
||||||
<i class="fa search-result-icon"></i>
|
<i class="fa search-result-icon"></i>
|
||||||
<span bo-text="row.title"></span>
|
{{::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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -106,17 +106,49 @@ export class SearchCtrl {
|
|||||||
this.currentSearchId = this.currentSearchId + 1;
|
this.currentSearchId = this.currentSearchId + 1;
|
||||||
var localSearchId = this.currentSearchId;
|
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; }
|
if (localSearchId < this.currentSearchId) { return; }
|
||||||
|
|
||||||
this.results = _.map(results, function(dash) {
|
let byId = _.groupBy(results, 'id');
|
||||||
dash.url = 'dashboard/' + dash.uri;
|
let byFolderId = _.groupBy(results, 'folderId');
|
||||||
return dash;
|
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()) {
|
// group by folder
|
||||||
this.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -211,7 +211,7 @@ export class BackendSrv {
|
|||||||
|
|
||||||
return this.post('/api/dashboards/db/', {
|
return this.post('/api/dashboards/db/', {
|
||||||
dashboard: dash,
|
dashboard: dash,
|
||||||
parentId: dash.parentId,
|
folderId: dash.folderId,
|
||||||
overwrite: options.overwrite === true,
|
overwrite: options.overwrite === true,
|
||||||
message: options.message || '',
|
message: options.message || '',
|
||||||
});
|
});
|
||||||
|
@@ -129,7 +129,7 @@ export class DashboardCtrl {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.onFolderChange = function(folder) {
|
$scope.onFolderChange = function(folder) {
|
||||||
$scope.dashboard.parentId = folder.id;
|
$scope.dashboard.folderId = folder.id;
|
||||||
$scope.dashboard.meta.folderId = folder.id;
|
$scope.dashboard.meta.folderId = folder.id;
|
||||||
$scope.dashboard.meta.folderTitle= folder.title;
|
$scope.dashboard.meta.folderTitle= folder.title;
|
||||||
};
|
};
|
||||||
|
@@ -140,8 +140,8 @@ export class DashNavCtrl {
|
|||||||
var newWindow = window.open(uri);
|
var newWindow = window.open(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFolderChange(parentId) {
|
onFolderChange(folderId) {
|
||||||
this.dashboard.parentId = parentId;
|
this.dashboard.folderId = folderId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ export class DashboardModel {
|
|||||||
meta: any;
|
meta: any;
|
||||||
events: any;
|
events: any;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
parentId: number;
|
folderId: number;
|
||||||
|
|
||||||
constructor(data, meta?) {
|
constructor(data, meta?) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -65,7 +65,7 @@ export class DashboardModel {
|
|||||||
this.version = data.version || 0;
|
this.version = data.version || 0;
|
||||||
this.links = data.links || [];
|
this.links = data.links || [];
|
||||||
this.gnetId = data.gnetId || null;
|
this.gnetId = data.gnetId || null;
|
||||||
this.parentId = data.parentId || null;
|
this.folderId = data.folderId || null;
|
||||||
|
|
||||||
this.rows = [];
|
this.rows = [];
|
||||||
if (data.rows) {
|
if (data.rows) {
|
||||||
|
@@ -73,7 +73,7 @@ export class SaveDashboardAsModalCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFolderChange(folder) {
|
onFolderChange(folder) {
|
||||||
this.clone.parentId = folder.id;
|
this.clone.folderId = folder.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -104,95 +104,45 @@
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search-item {
|
.search-item {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: $tight-form-bg;
|
background-color: $tight-form-bg;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
@include left-brand-border();
|
@include left-brand-border();
|
||||||
|
|
||||||
&:hover,
|
&:hover {
|
||||||
&.selected {
|
@include left-brand-border-gradient();
|
||||||
background-color: $tight-form-func-bg;
|
background-color: $tight-form-func-bg;
|
||||||
@include left-brand-border-gradient();
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: $tight-form-func-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dash-db,
|
||||||
|
&--dash-child {
|
||||||
|
.search-result-icon::before {
|
||||||
|
content: "\f009";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result-tags {
|
&--dash-folder {
|
||||||
float: right;
|
.search-result-icon::before {
|
||||||
}
|
content: "\f07c";
|
||||||
|
|
||||||
.search-result-actions {
|
|
||||||
float: right;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
&--dash-child {
|
||||||
.search-dropdown {
|
margin-left: 20px;
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.search-button-row {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
.search-result-tags {
|
||||||
.search-container {
|
float: right;
|
||||||
left: 78px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user