Expose folder UID in dashboards API response (#33991) (#34772)

* expose folder UID in dashboards API response, import dashboards into folders by folder UID

* handle bad folder UID as 400 error

* 12591:Add tests for request with folderUid

* Use more descriptive error status for missing folders

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

* return 400 when folder id is missing

* put error checking in the right place this time

* mention folderUid in the docs

* Clarify usage of folderUid and folderId when both present

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

* Capitalise UID

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* mention folder UID in the metadata for a GET response

Co-authored-by: Ida Furjesova <ida.furjesova@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
(cherry picked from commit ef0fab9aa5)

Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com>
This commit is contained in:
Grot (@grafanabot)
2021-05-26 22:10:59 +02:00
committed by GitHub
co-authored by Serge Zaitsev
parent fca62d20d6
commit 653a3bdfbf
6 changed files with 125 additions and 5 deletions
+24 -1
View File
@@ -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 {
+93 -3
View File
@@ -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)
+1
View File
@@ -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"`
+1
View File
@@ -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 {