Commit 0e8377a9 by Marcus Efraimsson Committed by Torkel Ödegaard

Update logic for create/update dashboard, validation and plugin dashboard links (#10809)

* enables overwrite if dashboard allready exist in folder

* dashboard: Don't allow creating a folder named General

* dashboards: update logic for save/update dashboard

No id and uid creates a new dashboard/folder.
No id and uid, with an existing title in folder allows overwrite
  of dashboard.
Id without uid, allows update of existing dashboard/folder without
  overwrite.
Uid without id allows update of existing dashboard/folder without
  overwrite.
Id without uid, with an existing title in folder allows overwrite
  of dashboard/folder and updated will have the uid of overwritten.
Uid without id, with an existing title in folder allows overwrite
  of dashboard/folder and new will have the same uid as provided.
Trying to change an existing folder to a dashboard yields error.
Trying to change an existing dashboard to a folder yields error.

* dashboards: include folder id when confirmed to save with overwrite

* dashboards: fixes due to new url structure

Return importedUrl property in response to importing dashboards and
getting plugin dashboards and use this for redirects/links in the
frontend.
parent fc05fc42
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
...@@ -217,6 +218,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { ...@@ -217,6 +218,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
} }
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(m.RootFolderName) {
return ApiError(400, "A folder already exists with that name", nil)
}
if dash.Id == 0 { if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard") limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil { if err != nil {
...@@ -237,8 +242,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { ...@@ -237,8 +242,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem) dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
if err == m.ErrDashboardTitleEmpty { if err == m.ErrDashboardTitleEmpty ||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) err == m.ErrDashboardWithSameNameAsFolder ||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
err == m.ErrDashboardTypeMismatch {
return ApiError(400, err.Error(), nil)
} }
if err == m.ErrDashboardContainsInvalidAlertData { if err == m.ErrDashboardContainsInvalidAlertData {
......
...@@ -13,17 +13,22 @@ import ( ...@@ -13,17 +13,22 @@ import (
// Typed errors // Typed errors
var ( var (
ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists") ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists") ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardExistingCannotChangeToDashboard = errors.New("An existing folder cannot be changed to a dashboard")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
RootFolderName = "General"
) )
type UpdatePluginDashboardError struct { type UpdatePluginDashboardError struct {
...@@ -95,14 +100,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { ...@@ -95,14 +100,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.Data = data dash.Data = data
dash.Title = dash.Data.Get("title").MustString() dash.Title = dash.Data.Get("title").MustString()
dash.UpdateSlug() dash.UpdateSlug()
update := false
if id, err := dash.Data.Get("id").Float64(); err == nil { if id, err := dash.Data.Get("id").Float64(); err == nil {
dash.Id = int64(id) dash.Id = int64(id)
update = true
}
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.Uid = uid
update = true
}
if version, err := dash.Data.Get("version").Float64(); err == nil { if version, err := dash.Data.Get("version").Float64(); err == nil && update {
dash.Version = int(version) dash.Version = int(version)
dash.Updated = time.Now() dash.Updated = time.Now()
}
} else { } else {
dash.Data.Set("version", 0) dash.Data.Set("version", 0)
dash.Created = time.Now() dash.Created = time.Now()
...@@ -113,10 +125,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { ...@@ -113,10 +125,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.GnetId = int64(gnetId) dash.GnetId = int64(gnetId)
} }
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.Uid = uid
}
return dash return dash
} }
......
...@@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { ...@@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
Path: cmd.Path, Path: cmd.Path,
Revision: dashboard.Data.Get("revision").MustInt64(1), Revision: dashboard.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug, ImportedUri: "db/" + saveCmd.Result.Slug,
ImportedUrl: saveCmd.Result.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true, Imported: true,
} }
......
...@@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct { ...@@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct {
Title string `json:"title"` Title string `json:"title"`
Imported bool `json:"imported"` Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"` ImportedUri string `json:"importedUri"`
ImportedUrl string `json:"importedUrl"`
Slug string `json:"slug"` Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"` ImportedRevision int64 `json:"importedRevision"`
...@@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT ...@@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
res.DashboardId = existingDash.Id res.DashboardId = existingDash.Id
res.Imported = true res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug res.ImportedUri = "db/" + existingDash.Slug
res.ImportedUrl = existingDash.GetUrl()
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1) res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
existingMatches[existingDash.Id] = true existingMatches[existingDash.Id] = true
} }
......
...@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
dash := cmd.GetDashboardModel() dash := cmd.GetDashboardModel()
// try get existing dashboard if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
var existing m.Dashboard return err
}
if dash.Id != 0 { var existingByTitleAndFolder m.Dashboard
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
// check for is someone else has written in between dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
if dash.Version != existing.Version { if err != nil {
if cmd.Overwrite { return err
dash.Version = existing.Version }
} else {
return m.ErrDashboardVersionMismatch if dashWithTitleAndFolderExists {
if dash.Id != existingByTitleAndFolder.Id {
if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
return m.ErrDashboardWithSameNameAsFolder
} }
}
// do not allow plugin dashboard updates without overwrite flag if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
if existing.PluginId != "" && cmd.Overwrite == false { return m.ErrDashboardFolderWithSameNameAsDashboard
return m.UpdatePluginDashboardError{PluginId: existing.PluginId} }
}
} else if dash.Uid != "" {
var sameUid m.Dashboard
sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid)
if err != nil {
return err
}
if sameUidExists { if cmd.Overwrite {
// another dashboard with same uid dash.Id = existingByTitleAndFolder.Id
if dash.Id != sameUid.Id { dash.Version = existingByTitleAndFolder.Version
if cmd.Overwrite {
dash.Id = sameUid.Id if dash.Uid == "" {
dash.Version = sameUid.Version dash.Uid = existingByTitleAndFolder.Uid
} else {
return m.ErrDashboardWithSameUIDExists
} }
} else {
return m.ErrDashboardWithSameNameInFolderExists
} }
} }
} }
...@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("uid", uid) dash.Data.Set("uid", uid)
} }
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
if err != nil {
return err
}
err = setHasAcl(sess, dash) err = setHasAcl(sess, dash)
if err != nil { if err != nil {
return err return err
...@@ -162,6 +146,72 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -162,6 +146,72 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}) })
} }
func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
dashWithIdExists := false
var existingById m.Dashboard
if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
if dash.Uid == "" {
dash.Uid = existingById.Uid
}
}
dashWithUidExists := false
var existingByUid m.Dashboard
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil {
return err
}
}
if !dashWithIdExists && !dashWithUidExists {
return nil
}
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
return m.ErrDashboardWithSameUIDExists
}
existing := existingById
if !dashWithIdExists && dashWithUidExists {
dash.Id = existingByUid.Id
existing = existingByUid
}
if (existing.IsFolder && !cmd.IsFolder) ||
(!existing.IsFolder && cmd.IsFolder) {
return m.ErrDashboardTypeMismatch
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.Version = existing.Version
} else {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
return nil
}
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
uid := generateNewUid() uid := generateNewUid()
...@@ -179,23 +229,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { ...@@ -179,23 +229,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
return "", m.ErrDashboardFailedGenerateUniqueUid return "", m.ErrDashboardFailedGenerateUniqueUid
} }
func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error {
var sameNameInFolder m.Dashboard
sameNameInFolderExist, err := sess.Where("org_id=? AND title=? AND folder_id = ? AND uid <> ?",
dash.OrgId, dash.Title, dash.FolderId, dash.Uid).
Get(&sameNameInFolder)
if err != nil {
return err
}
if sameNameInFolderExist {
return m.ErrDashboardWithSameNameInFolderExists
}
return nil
}
func setHasAcl(sess *DBSession, dash *m.Dashboard) error { func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
// check if parent has acl // check if parent has acl
if dash.FolderId > 0 { if dash.FolderId > 0 {
...@@ -518,9 +551,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery ...@@ -518,9 +551,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
params = append(params, query.UserId) params = append(params, query.UserId)
params = append(params, dialect.BooleanStr(false)) params = append(params, dialect.BooleanStr(false))
x.ShowSQL(true)
err := x.Sql(sql, params...).Find(&query.Result) err := x.Sql(sql, params...).Find(&query.Result)
x.ShowSQL(false)
for _, p := range query.Result { for _, p := range query.Result {
p.PermissionName = p.Permission.String() p.PermissionName = p.Permission.String()
......
...@@ -100,7 +100,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -100,7 +100,7 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
Convey("Should return error if no dashboard is updated", func() { Convey("Should return not found error if no dashboard is found for update", func() {
cmd := m.SaveDashboardCommand{ cmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Overwrite: true, Overwrite: true,
...@@ -112,7 +112,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -112,7 +112,7 @@ func TestDashboardDataAccess(t *testing.T) {
} }
err := SaveDashboard(&cmd) err := SaveDashboard(&cmd)
So(err, ShouldNotBeNil) So(err, ShouldEqual, m.ErrDashboardNotFound)
}) })
Convey("Should not be able to overwrite dashboard in another org", func() { Convey("Should not be able to overwrite dashboard in another org", func() {
...@@ -130,108 +130,171 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -130,108 +130,171 @@ func TestDashboardDataAccess(t *testing.T) {
} }
err := SaveDashboard(&cmd) err := SaveDashboard(&cmd)
So(err, ShouldNotBeNil) So(err, ShouldEqual, m.ErrDashboardNotFound)
}) })
Convey("Should be able to search for dashboard folder", func() { Convey("Should be able to save dashboards with same name in different folders", func() {
query := search.FindPersistedDashboardsQuery{ firstSaveCmd := m.SaveDashboardCommand{
Title: "1 test dash folder", OrgId: 1,
OrgId: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{
SignedInUser: &m.SignedInUser{OrgId: 1}, "id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "randomHash",
}),
FolderId: 3,
} }
err := SearchDashboards(&query) err := SaveDashboard(&firstSaveCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1) secondSaveCmd := m.SaveDashboardCommand{
hit := query.Result[0] OrgId: 1,
So(hit.Type, ShouldEqual, search.DashHitFolder) Dashboard: simplejson.NewFromAny(map[string]interface{}{
So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) "id": nil,
So(hit.FolderTitle, ShouldEqual, "") "title": "test dash folder and title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}),
FolderId: 1,
}
err = SaveDashboard(&secondSaveCmd)
So(err, ShouldBeNil)
So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
}) })
Convey("Should be able to search for a dashboard folder's children", func() { Convey("Should be able to overwrite dashboard in same folder using title", func() {
query := search.FindPersistedDashboardsQuery{ insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
OrgId: 1, folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
FolderIds: []int64{savedFolder.Id}, dashInFolder := insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
SignedInUser: &m.SignedInUser{OrgId: 1},
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: folder.Id,
Overwrite: true,
} }
err := SearchDashboards(&query) err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInFolder.Id)
So(cmd.Result.Uid, ShouldEqual, dashInFolder.Uid)
})
So(len(query.Result), ShouldEqual, 2) Convey("Should be able to overwrite dashboard in General folder using title", func() {
hit := query.Result[0] dashInGeneral := insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
So(hit.Id, ShouldEqual, savedDash.Id) folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug)) insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
So(hit.FolderId, ShouldEqual, savedFolder.Id)
So(hit.FolderUid, ShouldEqual, savedFolder.Uid) cmd := m.SaveDashboardCommand{
So(hit.FolderTitle, ShouldEqual, savedFolder.Title) OrgId: 1,
So(hit.FolderUrl, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInGeneral.Id)
So(cmd.Result.Uid, ShouldEqual, dashInGeneral.Uid)
}) })
Convey("Should be able to search for dashboard by dashboard ids", func() { Convey("Should not be able to overwrite folder with dashboard in general folder using title", func() {
Convey("should be able to find two dashboards by id", func() { cmd := m.SaveDashboardCommand{
query := search.FindPersistedDashboardsQuery{ OrgId: 1,
DashboardIds: []int64{2, 3}, Dashboard: simplejson.NewFromAny(map[string]interface{}{
SignedInUser: &m.SignedInUser{OrgId: 1}, "title": savedFolder.Title,
} }),
FolderId: 0,
IsFolder: false,
Overwrite: true,
}
err := SearchDashboards(&query) err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
So(len(query.Result), ShouldEqual, 2) Convey("Should not be able to overwrite folder with dashboard in folder using title", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
FolderId: savedFolder.Id,
IsFolder: false,
Overwrite: true,
}
hit := query.Result[0] err := SaveDashboard(&cmd)
So(len(hit.Tags), ShouldEqual, 2) So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
hit2 := query.Result[1] Convey("Should not be able to overwrite folder with dashboard using id", func() {
So(len(hit2.Tags), ShouldEqual, 1) cmd := m.SaveDashboardCommand{
}) OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: true,
}
Convey("DashboardIds that does not exists should not cause errors", func() { err := SaveDashboard(&cmd)
query := search.FindPersistedDashboardsQuery{ So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
DashboardIds: []int64{1000}, })
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query) Convey("Should not be able to overwrite dashboard with folder using id", func() {
So(err, ShouldBeNil) cmd := m.SaveDashboardCommand{
So(len(query.Result), ShouldEqual, 0) OrgId: 1,
}) Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDash.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
}) })
Convey("Should be able to save dashboards with same name in different folders", func() { Convey("Should not be able to overwrite folder with dashboard using uid", func() {
firstSaveCmd := m.SaveDashboardCommand{ cmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil, "uid": savedFolder.Uid,
"title": "test dash folder and title", "title": "new title",
"tags": []interface{}{},
"uid": "randomHash",
}), }),
FolderId: 3, IsFolder: false,
Overwrite: true,
} }
err := SaveDashboard(&firstSaveCmd) err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
secondSaveCmd := m.SaveDashboardCommand{ Convey("Should not be able to overwrite dashboard with folder using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil, "uid": savedDash.Uid,
"title": "test dash folder and title", "title": "new folder title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}), }),
FolderId: 1, IsFolder: true,
Overwrite: true,
} }
err = SaveDashboard(&secondSaveCmd) err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
}) })
Convey("Should not be able to save dashboard with same name in the same folder", func() { Convey("Should not be able to save dashboard with same name in the same folder without overwrite", func() {
firstSaveCmd := m.SaveDashboardCommand{ firstSaveCmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
...@@ -261,20 +324,49 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -261,20 +324,49 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists) So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
}) })
Convey("Should not be able to save dashboard with same uid", func() { Convey("Should be able to save and update dashboard using same uid", func() {
cmd := m.SaveDashboardCommand{ cmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil, "id": nil,
"title": "test dash 23",
"uid": "dsfalkjngailuedt", "uid": "dsfalkjngailuedt",
"title": "test dash 23",
}), }),
} }
err := SaveDashboard(&cmd) err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = SaveDashboard(&cmd) err = SaveDashboard(&cmd)
So(err, ShouldNotBeNil) So(err, ShouldBeNil)
})
Convey("Should be able to update dashboard using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "new title",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
Convey("Should be able to get updated dashboard by uid", func() {
query := m.GetDashboardQuery{
Uid: savedDash.Uid,
OrgId: 1,
}
err := GetDashboard(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, savedDash.Id)
So(query.Result.Title, ShouldEqual, "new title")
So(query.Result.FolderId, ShouldEqual, 0)
})
}) })
Convey("Should be able to update dashboard with the same title and folder id", func() { Convey("Should be able to update dashboard with the same title and folder id", func() {
...@@ -310,7 +402,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -310,7 +402,7 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
Convey("Should not be able to update using just uid", func() { Convey("Should be able to update using uid without id and overwrite", func() {
cmd := m.SaveDashboardCommand{ cmd := m.SaveDashboardCommand{
OrgId: 1, OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{ Dashboard: simplejson.NewFromAny(map[string]interface{}{
...@@ -323,23 +415,6 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -323,23 +415,6 @@ func TestDashboardDataAccess(t *testing.T) {
} }
err := SaveDashboard(&cmd) err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardWithSameUIDExists)
})
Convey("Should be able to update using just uid with overwrite", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "folderId",
"version": savedDash.Version,
"tags": []interface{}{},
}),
FolderId: savedDash.FolderId,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
...@@ -367,11 +442,11 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -367,11 +442,11 @@ func TestDashboardDataAccess(t *testing.T) {
generateNewUid = util.GenerateShortUid generateNewUid = util.GenerateShortUid
}) })
Convey("Should be able to update dashboard and remove folderId", func() { Convey("Should be able to update dashboard by id 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": savedDash.Id,
"title": "folderId", "title": "folderId",
"tags": []interface{}{}, "tags": []interface{}{},
}), }),
...@@ -386,7 +461,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -386,7 +461,7 @@ func TestDashboardDataAccess(t *testing.T) {
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": savedDash.Id,
"title": "folderId", "title": "folderId",
"tags": []interface{}{}, "tags": []interface{}{},
}), }),
...@@ -398,7 +473,7 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -398,7 +473,7 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
query := m.GetDashboardQuery{ query := m.GetDashboardQuery{
Slug: cmd.Result.Slug, Id: savedDash.Id,
OrgId: 1, OrgId: 1,
} }
...@@ -433,6 +508,63 @@ func TestDashboardDataAccess(t *testing.T) { ...@@ -433,6 +508,63 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 2) So(len(query.Result), ShouldEqual, 2)
}) })
Convey("Should be able to search for dashboard folder", func() {
query := search.FindPersistedDashboardsQuery{
Title: "1 test dash folder",
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
hit := query.Result[0]
So(hit.Type, ShouldEqual, search.DashHitFolder)
So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
So(hit.FolderTitle, ShouldEqual, "")
})
Convey("Should be able to search for a dashboard folder's children", func() {
query := search.FindPersistedDashboardsQuery{
OrgId: 1,
FolderIds: []int64{savedFolder.Id},
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
hit := query.Result[0]
So(hit.Id, ShouldEqual, savedDash.Id)
So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug))
So(hit.FolderId, ShouldEqual, savedFolder.Id)
So(hit.FolderUid, ShouldEqual, savedFolder.Uid)
So(hit.FolderTitle, ShouldEqual, savedFolder.Title)
So(hit.FolderUrl, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
})
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: []int64{2, 3},
SignedInUser: &m.SignedInUser{OrgId: 1},
}
err := SearchDashboards(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
hit := query.Result[0]
So(len(hit.Tags), ShouldEqual, 2)
hit2 := query.Result[1]
So(len(hit2.Tags), ShouldEqual, 1)
})
})
Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() { Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
starredDash := insertTestDashboard("starred dash", 1, 0, false) starredDash := insertTestDashboard("starred dash", 1, 0, false)
StarDashboard(&m.StarDashboardCommand{ StarDashboard(&m.StarDashboardCommand{
......
...@@ -18,7 +18,7 @@ export class DashboardImportCtrl { ...@@ -18,7 +18,7 @@ export class DashboardImportCtrl {
nameValidationError: any; nameValidationError: any;
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, private $scope, $routeParams) { constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
this.navModel = navModelSrv.getNav('create', 'import'); this.navModel = navModelSrv.getNav('create', 'import');
this.step = 1; this.step = 1;
...@@ -124,8 +124,7 @@ export class DashboardImportCtrl { ...@@ -124,8 +124,7 @@ export class DashboardImportCtrl {
inputs: inputs, inputs: inputs,
}) })
.then(res => { .then(res => {
this.$location.url('dashboard/' + res.importedUri); this.$location.url(res.importedUrl);
this.$scope.dismiss();
}); });
} }
......
...@@ -20,7 +20,10 @@ export class DashboardSrv { ...@@ -20,7 +20,10 @@ export class DashboardSrv {
return this.dash; return this.dash;
} }
handleSaveDashboardError(clone, err) { handleSaveDashboardError(clone, options, err) {
options = options || {};
options.overwrite = true;
if (err.data && err.data.status === 'version-mismatch') { if (err.data && err.data.status === 'version-mismatch') {
err.isHandled = true; err.isHandled = true;
...@@ -31,7 +34,7 @@ export class DashboardSrv { ...@@ -31,7 +34,7 @@ export class DashboardSrv {
yesText: 'Save & Overwrite', yesText: 'Save & Overwrite',
icon: 'fa-warning', icon: 'fa-warning',
onConfirm: () => { onConfirm: () => {
this.save(clone, { overwrite: true }); this.save(clone, options);
}, },
}); });
} }
...@@ -41,12 +44,12 @@ export class DashboardSrv { ...@@ -41,12 +44,12 @@ export class DashboardSrv {
this.$rootScope.appEvent('confirm-modal', { this.$rootScope.appEvent('confirm-modal', {
title: 'Conflict', title: 'Conflict',
text: 'Dashboard with the same name exists.', text: 'A dashboard with the same name in selected folder already exists.',
text2: 'Would you still like to save this dashboard?', text2: 'Would you still like to save this dashboard?',
yesText: 'Save & Overwrite', yesText: 'Save & Overwrite',
icon: 'fa-warning', icon: 'fa-warning',
onConfirm: () => { onConfirm: () => {
this.save(clone, { overwrite: true }); this.save(clone, options);
}, },
}); });
} }
...@@ -91,7 +94,7 @@ export class DashboardSrv { ...@@ -91,7 +94,7 @@ export class DashboardSrv {
return this.backendSrv return this.backendSrv
.saveDashboard(clone, options) .saveDashboard(clone, options)
.then(this.postSave.bind(this, clone)) .then(this.postSave.bind(this, clone))
.catch(this.handleSaveDashboardError.bind(this, clone)); .catch(this.handleSaveDashboardError.bind(this, clone, options));
} }
saveDashboard(options, clone) { saveDashboard(options, clone) {
......
...@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() { ...@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() {
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()), validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
}; };
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}, {}); ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
}); });
describe('when uploading json', function() { describe('when uploading json', function() {
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<i class="icon-gf icon-gf-dashboard"></i> <i class="icon-gf icon-gf-dashboard"></i>
</td> </td>
<td> <td>
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported"> <a href="{{dash.importedUrl}}" ng-show="dash.imported">
{{dash.title}} {{dash.title}}
</a> </a>
<span ng-show="!dash.imported"> <span ng-show="!dash.imported">
......
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