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 (
"fmt"
"os"
"path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards"
......@@ -217,6 +218,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
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 {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil {
......@@ -237,8 +242,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
if err == m.ErrDashboardTitleEmpty {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
if err == m.ErrDashboardTitleEmpty ||
err == m.ErrDashboardWithSameNameAsFolder ||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
err == m.ErrDashboardTypeMismatch {
return ApiError(400, err.Error(), nil)
}
if err == m.ErrDashboardContainsInvalidAlertData {
......
......@@ -13,17 +13,22 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
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")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
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")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
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 {
......@@ -95,14 +100,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.Data = data
dash.Title = dash.Data.Get("title").MustString()
dash.UpdateSlug()
update := false
if id, err := dash.Data.Get("id").Float64(); err == nil {
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 {
dash.Version = int(version)
dash.Updated = time.Now()
}
if version, err := dash.Data.Get("version").Float64(); err == nil && update {
dash.Version = int(version)
dash.Updated = time.Now()
} else {
dash.Data.Set("version", 0)
dash.Created = time.Now()
......@@ -113,10 +125,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.GnetId = int64(gnetId)
}
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.Uid = uid
}
return dash
}
......
......@@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
Path: cmd.Path,
Revision: dashboard.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug,
ImportedUrl: saveCmd.Result.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true,
}
......
......@@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct {
Title string `json:"title"`
Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"`
ImportedUrl string `json:"importedUrl"`
Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"`
......@@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
res.DashboardId = existingDash.Id
res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug
res.ImportedUrl = existingDash.GetUrl()
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
existingMatches[existingDash.Id] = true
}
......
......@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
dash := cmd.GetDashboardModel()
// try get existing dashboard
var existing m.Dashboard
if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
return err
}
if dash.Id != 0 {
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
var existingByTitleAndFolder m.Dashboard
// 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
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 err != nil {
return err
}
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 existing.PluginId != "" && cmd.Overwrite == false {
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 !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if sameUidExists {
// another dashboard with same uid
if dash.Id != sameUid.Id {
if cmd.Overwrite {
dash.Id = sameUid.Id
dash.Version = sameUid.Version
} else {
return m.ErrDashboardWithSameUIDExists
if cmd.Overwrite {
dash.Id = existingByTitleAndFolder.Id
dash.Version = existingByTitleAndFolder.Version
if dash.Uid == "" {
dash.Uid = existingByTitleAndFolder.Uid
}
} else {
return m.ErrDashboardWithSameNameInFolderExists
}
}
}
......@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("uid", uid)
}
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
if err != nil {
return err
}
err = setHasAcl(sess, dash)
if err != nil {
return err
......@@ -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) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
......@@ -179,23 +229,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
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 {
// check if parent has acl
if dash.FolderId > 0 {
......@@ -518,9 +551,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
params = append(params, query.UserId)
params = append(params, dialect.BooleanStr(false))
x.ShowSQL(true)
err := x.Sql(sql, params...).Find(&query.Result)
x.ShowSQL(false)
for _, p := range query.Result {
p.PermissionName = p.Permission.String()
......
......@@ -18,7 +18,7 @@ export class DashboardImportCtrl {
nameValidationError: any;
/** @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.step = 1;
......@@ -124,8 +124,7 @@ export class DashboardImportCtrl {
inputs: inputs,
})
.then(res => {
this.$location.url('dashboard/' + res.importedUri);
this.$scope.dismiss();
this.$location.url(res.importedUrl);
});
}
......
......@@ -20,7 +20,10 @@ export class DashboardSrv {
return this.dash;
}
handleSaveDashboardError(clone, err) {
handleSaveDashboardError(clone, options, err) {
options = options || {};
options.overwrite = true;
if (err.data && err.data.status === 'version-mismatch') {
err.isHandled = true;
......@@ -31,7 +34,7 @@ export class DashboardSrv {
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
this.save(clone, { overwrite: true });
this.save(clone, options);
},
});
}
......@@ -41,12 +44,12 @@ export class DashboardSrv {
this.$rootScope.appEvent('confirm-modal', {
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?',
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
this.save(clone, { overwrite: true });
this.save(clone, options);
},
});
}
......@@ -91,7 +94,7 @@ export class DashboardSrv {
return this.backendSrv
.saveDashboard(clone, options)
.then(this.postSave.bind(this, clone))
.catch(this.handleSaveDashboardError.bind(this, clone));
.catch(this.handleSaveDashboardError.bind(this, clone, options));
}
saveDashboard(options, clone) {
......
......@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() {
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() {
......
......@@ -6,7 +6,7 @@
<i class="icon-gf icon-gf-dashboard"></i>
</td>
<td>
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
<a href="{{dash.importedUrl}}" ng-show="dash.imported">
{{dash.title}}
</a>
<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