diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 61f1052c278..b64d61f7e19 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -44,6 +44,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "refresh": "25s" }, "folderId": 0, + "folderUid": "l3KqBxCMz", "message": "Made changes to xyz", "overwrite": false } @@ -55,6 +56,7 @@ JSON Body schema: - **dashboard.id** – id = null to create a new dashboard. - **dashboard.uid** – Optional unique identifier when creating a dashboard. uid = null will generate a new uid. - **folderId** – The id of the folder to save the dashboard in. +- **folderUid** – The UID of the folder to save the dashboard in. Overrides the `folderId`. - **overwrite** – Set to true if you want to overwrite existing dashboard with newer version, same dashboard title in folder or same dashboard uid. - **message** - Set a commit message for the version history. - **refresh** - Set the dashboard refresh interval. If this is lower than [the minimum refresh interval]({{< relref "../administration/configuration.md#min_refresh_interval">}}), then Grafana will ignore it and will enforce the minimum refresh interval. @@ -268,7 +270,7 @@ In case of title already exists the `status` property will be `name-exists`. `GET /api/dashboards/uid/:uid` -Will return the dashboard given the dashboard unique identifier (uid). +Will return the dashboard given the dashboard unique identifier (uid). Information about the unique identifier of a folder containing the requested dashboard might be found in the metadata. **Example Request**: @@ -298,6 +300,8 @@ Content-Type: application/json "meta": { "isStarred": false, "url": "/d/cIBgcSjkk/production-overview", + "folderId": 2, + "folderUid": "l3KqBxCMz", "slug": "production-overview" //deprecated in Grafana v5.0 } } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8438a13c397..3bd65069afd 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -137,8 +137,12 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { if dash.FolderId > 0 { query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { + if errors.Is(err, models.ErrFolderNotFound) { + return response.Error(404, "Folder not found", err) + } return response.Error(500, "Dashboard folder could not be read", err) } + meta.FolderUid = query.Result.Uid meta.FolderTitle = query.Result.Title meta.FolderUrl = query.Result.GetUrl() } @@ -275,8 +279,27 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa var err error cmd.OrgId = c.OrgId cmd.UserId = c.UserId + trimDefaults := c.QueryBoolWithDefault("trimdefaults", false) + if trimDefaults && !hs.LoadSchemaService.IsDisabled() { + cmd.Dashboard, err = hs.LoadSchemaService.DashboardApplyDefaults(cmd.Dashboard) + if err != nil { + return response.Error(500, "Error while applying default value to the dashboard json", err) + } + } + if cmd.FolderUid != "" { + folders := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) + folder, err := folders.GetFolderByUID(cmd.FolderUid) + if err != nil { + if errors.Is(err, models.ErrFolderNotFound) { + return response.Error(400, "Folder not found", err) + } + return response.Error(500, "Error while checking folder ID", err) + } + cmd.FolderId = folder.Id + } + dash := cmd.GetDashboardModel() - newDashboard := dash.Id == 0 && dash.Uid == "" + newDashboard := dash.Id == 0 if newDashboard { limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard") if err != nil { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index fd500c617af..ac934acd929 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "fmt" "io/ioutil" "path/filepath" @@ -799,7 +800,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { }, } - postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) dto := mock.SavedDashboards[0] @@ -819,6 +820,91 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) }) + t.Run("Given a correct request for creating a dashboard with folder uid", func(t *testing.T) { + const folderUid string = "folderUID" + const dashID int64 = 2 + + cmd := models.SaveDashboardCommand{ + OrgId: 1, + UserId: 5, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash", + }), + Overwrite: true, + FolderUid: folderUid, + IsFolder: false, + Message: "msg", + } + + mock := &dashboards.FakeDashboardService{ + SaveDashboardResult: &models.Dashboard{ + Id: dashID, + Uid: "uid", + Title: "Dash", + Slug: "dash", + Version: 2, + }, + } + + mockFolder := &fakeFolderService{ + GetFolderByUIDResult: &models.Folder{Id: 1, Uid: "folderUID", Title: "Folder"}, + } + + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) { + callPostDashboardShouldReturnSuccess(sc) + + dto := mock.SavedDashboards[0] + assert.Equal(t, cmd.OrgId, dto.OrgId) + assert.Equal(t, cmd.UserId, dto.User.UserId) + assert.Equal(t, "Dash", dto.Dashboard.Title) + assert.True(t, dto.Overwrite) + assert.Equal(t, "msg", dto.Message) + + result := sc.ToJSON() + assert.Equal(t, "success", result.Get("status").MustString()) + assert.Equal(t, dashID, result.Get("id").MustInt64()) + assert.Equal(t, "uid", result.Get("uid").MustString()) + assert.Equal(t, "dash", result.Get("slug").MustString()) + assert.Equal(t, "/d/uid/dash", result.Get("url").MustString()) + }) + }) + + t.Run("Given a request with incorrect folder uid for creating a dashboard with", func(t *testing.T) { + const folderUid string = "folderUID" + const dashID int64 = 2 + + cmd := models.SaveDashboardCommand{ + OrgId: 1, + UserId: 5, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash", + }), + Overwrite: true, + FolderUid: folderUid, + IsFolder: false, + Message: "msg", + } + + mock := &dashboards.FakeDashboardService{ + SaveDashboardResult: &models.Dashboard{ + Id: dashID, + Uid: "uid", + Title: "Dash", + Slug: "dash", + Version: 2, + }, + } + + mockFolder := &fakeFolderService{ + GetFolderByUIDError: errors.New("Error while searching Folder ID"), + } + + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) { + callPostDashboard(sc) + assert.Equal(t, 500, sc.resp.Code) + }) + }) + // This tests that invalid requests returns expected error responses t.Run("Given incorrect requests for creating a dashboard", func(t *testing.T) { testCases := []struct { @@ -858,7 +944,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { } postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), - "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { + "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) { callPostDashboard(sc) assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code) }) @@ -1207,7 +1293,8 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) { } func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, - mock *dashboards.FakeDashboardService, cmd models.SaveDashboardCommand, fn scenarioFunc) { + mock *dashboards.FakeDashboardService, mockFolder *fakeFolderService, cmd models.SaveDashboardCommand, + fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Cleanup(bus.ClearBusHandlers) @@ -1235,14 +1322,17 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s origNewDashboardService := dashboards.NewService origProvisioningService := dashboards.NewProvisioningService + origNewFolderService := dashboards.NewFolderService t.Cleanup(func() { dashboards.NewService = origNewDashboardService dashboards.NewProvisioningService = origProvisioningService + dashboards.NewFolderService = origNewFolderService }) dashboards.MockDashboardService(mock) dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { return mockDashboardProvisioningService{} } + mockFolderService(mockFolder) sc.m.Post(routePattern, sc.defaultHandler) diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 83edfbe08d7..22dea05b499 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -26,6 +26,7 @@ type DashboardMeta struct { HasAcl bool `json:"hasAcl"` IsFolder bool `json:"isFolder"` FolderId int64 `json:"folderId"` + FolderUid string `json:"folderUid"` FolderTitle string `json:"folderTitle"` FolderUrl string `json:"folderUrl"` Provisioned bool `json:"provisioned"` diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 15ff6938b2d..835e2d22ebb 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -65,6 +65,7 @@ type ImportDashboardCommand struct { Dashboard *simplejson.Json `json:"dashboard"` Inputs []plugins.ImportDashboardInput `json:"inputs"` FolderId int64 `json:"folderId"` + FolderUid string `json:"folderUid"` } type InstallPluginCommand struct { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 055a2bf4bb5..c843e023b5c 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -339,6 +339,7 @@ type SaveDashboardCommand struct { RestoredFrom int `json:"-"` PluginId string `json:"-"` FolderId int64 `json:"folderId"` + FolderUid string `json:"folderUid"` IsFolder bool `json:"isFolder"` UpdatedAt time.Time