mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
Expose folder UID in dashboards API response (#33991)
* 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>
This commit is contained in:
parent
9016d20c4c
commit
ef0fab9aa5
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"`
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user