mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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"
|
"refresh": "25s"
|
||||||
},
|
},
|
||||||
"folderId": 0,
|
"folderId": 0,
|
||||||
|
"folderUid": "l3KqBxCMz",
|
||||||
"message": "Made changes to xyz",
|
"message": "Made changes to xyz",
|
||||||
"overwrite": false
|
"overwrite": false
|
||||||
}
|
}
|
||||||
@ -55,6 +56,7 @@ JSON Body schema:
|
|||||||
- **dashboard.id** – id = null to create a new dashboard.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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`
|
`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**:
|
**Example Request**:
|
||||||
|
|
||||||
@ -298,6 +300,8 @@ Content-Type: application/json
|
|||||||
"meta": {
|
"meta": {
|
||||||
"isStarred": false,
|
"isStarred": false,
|
||||||
"url": "/d/cIBgcSjkk/production-overview",
|
"url": "/d/cIBgcSjkk/production-overview",
|
||||||
|
"folderId": 2,
|
||||||
|
"folderUid": "l3KqBxCMz",
|
||||||
"slug": "production-overview" //deprecated in Grafana v5.0
|
"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 {
|
if dash.FolderId > 0 {
|
||||||
query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
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)
|
return response.Error(500, "Dashboard folder could not be read", err)
|
||||||
}
|
}
|
||||||
|
meta.FolderUid = query.Result.Uid
|
||||||
meta.FolderTitle = query.Result.Title
|
meta.FolderTitle = query.Result.Title
|
||||||
meta.FolderUrl = query.Result.GetUrl()
|
meta.FolderUrl = query.Result.GetUrl()
|
||||||
}
|
}
|
||||||
@ -275,8 +279,27 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
|||||||
var err error
|
var err error
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
cmd.UserId = c.UserId
|
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()
|
dash := cmd.GetDashboardModel()
|
||||||
newDashboard := dash.Id == 0 && dash.Uid == ""
|
newDashboard := dash.Id == 0
|
||||||
if newDashboard {
|
if newDashboard {
|
||||||
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"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)
|
callPostDashboardShouldReturnSuccess(sc)
|
||||||
|
|
||||||
dto := mock.SavedDashboards[0]
|
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
|
// This tests that invalid requests returns expected error responses
|
||||||
t.Run("Given incorrect requests for creating a dashboard", func(t *testing.T) {
|
t.Run("Given incorrect requests for creating a dashboard", func(t *testing.T) {
|
||||||
testCases := []struct {
|
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()),
|
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)
|
callPostDashboard(sc)
|
||||||
assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code)
|
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,
|
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.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
t.Cleanup(bus.ClearBusHandlers)
|
t.Cleanup(bus.ClearBusHandlers)
|
||||||
|
|
||||||
@ -1235,14 +1322,17 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
|||||||
|
|
||||||
origNewDashboardService := dashboards.NewService
|
origNewDashboardService := dashboards.NewService
|
||||||
origProvisioningService := dashboards.NewProvisioningService
|
origProvisioningService := dashboards.NewProvisioningService
|
||||||
|
origNewFolderService := dashboards.NewFolderService
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
dashboards.NewService = origNewDashboardService
|
dashboards.NewService = origNewDashboardService
|
||||||
dashboards.NewProvisioningService = origProvisioningService
|
dashboards.NewProvisioningService = origProvisioningService
|
||||||
|
dashboards.NewFolderService = origNewFolderService
|
||||||
})
|
})
|
||||||
dashboards.MockDashboardService(mock)
|
dashboards.MockDashboardService(mock)
|
||||||
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
|
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
|
||||||
return mockDashboardProvisioningService{}
|
return mockDashboardProvisioningService{}
|
||||||
}
|
}
|
||||||
|
mockFolderService(mockFolder)
|
||||||
|
|
||||||
sc.m.Post(routePattern, sc.defaultHandler)
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ type DashboardMeta struct {
|
|||||||
HasAcl bool `json:"hasAcl"`
|
HasAcl bool `json:"hasAcl"`
|
||||||
IsFolder bool `json:"isFolder"`
|
IsFolder bool `json:"isFolder"`
|
||||||
FolderId int64 `json:"folderId"`
|
FolderId int64 `json:"folderId"`
|
||||||
|
FolderUid string `json:"folderUid"`
|
||||||
FolderTitle string `json:"folderTitle"`
|
FolderTitle string `json:"folderTitle"`
|
||||||
FolderUrl string `json:"folderUrl"`
|
FolderUrl string `json:"folderUrl"`
|
||||||
Provisioned bool `json:"provisioned"`
|
Provisioned bool `json:"provisioned"`
|
||||||
|
@ -65,6 +65,7 @@ type ImportDashboardCommand struct {
|
|||||||
Dashboard *simplejson.Json `json:"dashboard"`
|
Dashboard *simplejson.Json `json:"dashboard"`
|
||||||
Inputs []plugins.ImportDashboardInput `json:"inputs"`
|
Inputs []plugins.ImportDashboardInput `json:"inputs"`
|
||||||
FolderId int64 `json:"folderId"`
|
FolderId int64 `json:"folderId"`
|
||||||
|
FolderUid string `json:"folderUid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstallPluginCommand struct {
|
type InstallPluginCommand struct {
|
||||||
|
@ -339,6 +339,7 @@ type SaveDashboardCommand struct {
|
|||||||
RestoredFrom int `json:"-"`
|
RestoredFrom int `json:"-"`
|
||||||
PluginId string `json:"-"`
|
PluginId string `json:"-"`
|
||||||
FolderId int64 `json:"folderId"`
|
FolderId int64 `json:"folderId"`
|
||||||
|
FolderUid string `json:"folderUid"`
|
||||||
IsFolder bool `json:"isFolder"`
|
IsFolder bool `json:"isFolder"`
|
||||||
|
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
Loading…
Reference in New Issue
Block a user