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 {
......
...@@ -24,6 +24,11 @@ var ( ...@@ -24,6 +24,11 @@ var (
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
}
var existingByTitleAndFolder m.Dashboard
if dash.Id != 0 { 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)
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil { if err != nil {
return err return err
} }
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
// check for is someone else has written in between if dashWithTitleAndFolderExists {
if dash.Version != existing.Version { if dash.Id != existingByTitleAndFolder.Id {
if cmd.Overwrite { if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
dash.Version = existing.Version return m.ErrDashboardWithSameNameAsFolder
} else {
return m.ErrDashboardVersionMismatch
}
} }
// 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 {
// another dashboard with same uid
if dash.Id != sameUid.Id {
if cmd.Overwrite { if cmd.Overwrite {
dash.Id = sameUid.Id dash.Id = existingByTitleAndFolder.Id
dash.Version = sameUid.Version dash.Version = existingByTitleAndFolder.Version
} else {
return m.ErrDashboardWithSameUIDExists if dash.Uid == "" {
dash.Uid = existingByTitleAndFolder.Uid
} }
} 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,40 +146,89 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -162,40 +146,89 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}) })
} }
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
for i := 0; i < 3; i++ { dashWithIdExists := false
uid := generateNewUid() var existingById m.Dashboard
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.Dashboard{}) if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil { if err != nil {
return "", err return err
} }
if !exists { if !dashWithIdExists {
return uid, nil return m.ErrDashboardNotFound
}
} }
return "", m.ErrDashboardFailedGenerateUniqueUid if dash.Uid == "" {
} dash.Uid = existingById.Uid
}
}
func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error { dashWithUidExists := false
var sameNameInFolder m.Dashboard var existingByUid 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 dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil { if err != nil {
return err return err
} }
}
if sameNameInFolderExist { if !dashWithIdExists && !dashWithUidExists {
return m.ErrDashboardWithSameNameInFolderExists 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 return nil
} }
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.Dashboard{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", m.ErrDashboardFailedGenerateUniqueUid
}
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()
......
...@@ -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