mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelLibrary: adding library panels to Dashboard Api (#30278)
* Wip: First naive impl * Chore: fix after merge * Chore: changes after PR comments * Chore: removes unused types * Chore: adds feature toggle * Refactor: adds library panels cleanup and connect when storing dashboards * Refactor: adds feature toggle * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: adds disconnect library panels when deleting a dashboard * Chore: changes after PR comments * Tests: adds tests for LoadLibraryPanelsForDashboard * Tests: adds tests for CleanLibraryPanelsForDashboard * Tests: adds tests for ConnectLibraryPanelsForDashboard * Tests: adds tests for DisconnectLibraryPanelsForDashboard and small refactor * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: deletes all connections in one call and connects all in the same transaction * Chore: adds better comments * Chore: changes after PR comments Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
36dc70e168
commit
b7b6632a4d
176
devenv/dev-dashboards/panel-library/panel-library.json
Normal file
176
devenv/dev-dashboards/panel-library/panel-library.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 66,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 5,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"alertThreshold": true
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "7.4.0-pre",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"alias": "",
|
||||
"csvWave": {
|
||||
"timeStep": 60,
|
||||
"valuesCSV": "0,0,2,2,1,1"
|
||||
},
|
||||
"lines": 10,
|
||||
"points": [],
|
||||
"pulseWave": {
|
||||
"offCount": 3,
|
||||
"offValue": 1,
|
||||
"onCount": 3,
|
||||
"onValue": 2,
|
||||
"timeStep": 60
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stream": {
|
||||
"bands": 1,
|
||||
"noise": 2.2,
|
||||
"speed": 250,
|
||||
"spread": 3.5,
|
||||
"type": "signal"
|
||||
},
|
||||
"stringInput": "1,20,90,30,5,0"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"libraryPanel": {
|
||||
"uid": "MAnX2ifMk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 16,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 2,
|
||||
"libraryPanel": {
|
||||
"uid": "g1sNpCaMz"
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 27,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel - Panel Library",
|
||||
"uid": "imQX6j-Gz",
|
||||
"version": 1
|
||||
}
|
@ -304,10 +304,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// Dashboard
|
||||
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
|
||||
dashboardRoute.Get("/uid/:uid", routing.Wrap(hs.GetDashboard))
|
||||
dashboardRoute.Delete("/uid/:uid", routing.Wrap(DeleteDashboardByUID))
|
||||
dashboardRoute.Delete("/uid/:uid", routing.Wrap(hs.DeleteDashboardByUID))
|
||||
|
||||
dashboardRoute.Get("/db/:slug", routing.Wrap(hs.GetDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", routing.Wrap(DeleteDashboardBySlug))
|
||||
dashboardRoute.Delete("/db/:slug", routing.Wrap(hs.DeleteDashboardBySlug))
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), routing.Wrap(CalculateDashboardDiff))
|
||||
|
||||
|
@ -147,6 +147,14 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
||||
// make sure db version is in sync with json model version
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
if hs.Cfg.IsPanelLibraryEnabled() {
|
||||
// load library panels JSON for this dashboard
|
||||
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(dash)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while loading library panels", err)
|
||||
}
|
||||
}
|
||||
|
||||
dto := dtos.DashboardFullWithMeta{
|
||||
Dashboard: dash.Data,
|
||||
Meta: meta,
|
||||
@ -181,7 +189,7 @@ func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*models
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func DeleteDashboardBySlug(c *models.ReqContext) response.Response {
|
||||
func (hs *HTTPServer) DeleteDashboardBySlug(c *models.ReqContext) response.Response {
|
||||
query := models.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
@ -192,14 +200,14 @@ func DeleteDashboardBySlug(c *models.ReqContext) response.Response {
|
||||
return response.JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": models.ErrDashboardsWithSameSlugExists.Error()})
|
||||
}
|
||||
|
||||
return deleteDashboard(c)
|
||||
return hs.deleteDashboard(c)
|
||||
}
|
||||
|
||||
func DeleteDashboardByUID(c *models.ReqContext) response.Response {
|
||||
return deleteDashboard(c)
|
||||
func (hs *HTTPServer) DeleteDashboardByUID(c *models.ReqContext) response.Response {
|
||||
return hs.deleteDashboard(c)
|
||||
}
|
||||
|
||||
func deleteDashboard(c *models.ReqContext) response.Response {
|
||||
func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
@ -210,6 +218,14 @@ func deleteDashboard(c *models.ReqContext) response.Response {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
if hs.Cfg.IsPanelLibraryEnabled() {
|
||||
// disconnect all library panels for this dashboard
|
||||
err := hs.LibraryPanelService.DisconnectLibraryPanelsForDashboard(dash)
|
||||
if err != nil {
|
||||
hs.log.Error("Failed to disconnect library panels", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := dashboards.NewService().DeleteDashboard(dash.Id, c.OrgId)
|
||||
if err != nil {
|
||||
var dashboardErr models.DashboardErr
|
||||
@ -256,6 +272,14 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name)
|
||||
}
|
||||
|
||||
if hs.Cfg.IsPanelLibraryEnabled() {
|
||||
// clean up all unnecessary library panels JSON properties so we store a minimum JSON
|
||||
err = hs.LibraryPanelService.CleanLibraryPanelsForDashboard(dash)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while cleaning library panels", err)
|
||||
}
|
||||
}
|
||||
|
||||
dashItem := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dash,
|
||||
Message: cmd.Message,
|
||||
@ -288,6 +312,14 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
}
|
||||
}
|
||||
|
||||
if hs.Cfg.IsPanelLibraryEnabled() {
|
||||
// connect library panels for this dashboard after the dashboard is stored and has an ID
|
||||
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dashboard)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while connecting library panels", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.MApiDashboardSave)
|
||||
return response.JSON(200, util.DynMap{
|
||||
"status": "success",
|
||||
|
@ -165,7 +165,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
@ -175,7 +175,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
@ -230,7 +230,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -239,7 +239,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -354,7 +354,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -363,7 +363,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -414,7 +414,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -423,7 +423,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUp()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -492,7 +492,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -500,7 +500,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -570,7 +570,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -578,7 +578,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -624,7 +624,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -632,7 +632,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -690,7 +690,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
|
||||
})
|
||||
@ -698,7 +698,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
state := setUpInner()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
|
||||
})
|
||||
@ -744,7 +744,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
role := models.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
|
||||
assert.Equal(t, 412, sc.resp.Code)
|
||||
result := sc.ToJSON()
|
||||
@ -1033,7 +1033,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
|
||||
CallDeleteDashboardBySlug(sc)
|
||||
CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
|
||||
assert.Equal(t, 400, sc.resp.Code)
|
||||
result := sc.ToJSON()
|
||||
@ -1043,7 +1043,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
|
||||
CallDeleteDashboardByUID(sc)
|
||||
CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()})
|
||||
|
||||
assert.Equal(t, 400, sc.resp.Code)
|
||||
result := sc.ToJSON()
|
||||
@ -1141,21 +1141,21 @@ func callGetDashboardVersions(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallDeleteDashboardBySlug(sc *scenarioContext) {
|
||||
func CallDeleteDashboardBySlug(sc *scenarioContext, hs *HTTPServer) {
|
||||
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = DeleteDashboardBySlug
|
||||
sc.handlerFunc = hs.DeleteDashboardBySlug
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallDeleteDashboardByUID(sc *scenarioContext) {
|
||||
func CallDeleteDashboardByUID(sc *scenarioContext, hs *HTTPServer) {
|
||||
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = DeleteDashboardByUID
|
||||
sc.handlerFunc = hs.DeleteDashboardByUID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@ -59,26 +60,27 @@ type HTTPServer struct {
|
||||
httpSrv *http.Server
|
||||
middlewares []macaron.Handler
|
||||
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *localcache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||
ProvisioningService provisioning.ProvisioningService `inject:""`
|
||||
Login *login.LoginService `inject:""`
|
||||
License models.Licensing `inject:""`
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
PluginManager *plugins.PluginManager `inject:""`
|
||||
SearchService *search.SearchService `inject:""`
|
||||
ShortURLService *shorturls.ShortURLService `inject:""`
|
||||
Live *live.GrafanaLive `inject:""`
|
||||
ContextHandler *contexthandler.ContextHandler `inject:""`
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *localcache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||
ProvisioningService provisioning.ProvisioningService `inject:""`
|
||||
Login *login.LoginService `inject:""`
|
||||
License models.Licensing `inject:""`
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
PluginManager *plugins.PluginManager `inject:""`
|
||||
SearchService *search.SearchService `inject:""`
|
||||
ShortURLService *shorturls.ShortURLService `inject:""`
|
||||
Live *live.GrafanaLive `inject:""`
|
||||
ContextHandler *contexthandler.ContextHandler `inject:""`
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
LibraryPanelService *librarypanels.LibraryPanelService `inject:""`
|
||||
Listener net.Listener
|
||||
}
|
||||
|
||||
|
@ -181,6 +181,17 @@ func (j *Json) GetIndex(index int) *Json {
|
||||
return &Json{nil}
|
||||
}
|
||||
|
||||
// SetIndex modifies `Json` array by `index` and `value`
|
||||
// for `index` in its `array` representation
|
||||
func (j *Json) SetIndex(index int, val interface{}) {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
a[index] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckGet returns a pointer to a new `Json` object and
|
||||
// a `bool` identifying success or failure
|
||||
//
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@ -40,27 +42,46 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre
|
||||
return libraryPanel, err
|
||||
}
|
||||
|
||||
func connectDashboard(session *sqlstore.DBSession, dialect migrator.Dialect, user *models.SignedInUser, uid string, dashboardID int64) error {
|
||||
panel, err := getLibraryPanel(session, uid, user.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO add check that dashboard exists
|
||||
|
||||
libraryPanelDashboard := libraryPanelDashboard{
|
||||
DashboardID: dashboardID,
|
||||
LibraryPanelID: panel.ID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: user.UserId,
|
||||
}
|
||||
if _, err := session.Insert(&libraryPanelDashboard); err != nil {
|
||||
if dialect.IsUniqueConstraintViolation(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectDashboard adds a connection between a Library Panel and a Dashboard.
|
||||
func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error {
|
||||
err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID)
|
||||
})
|
||||
|
||||
// TODO add check that dashboard exists
|
||||
return err
|
||||
}
|
||||
|
||||
libraryPanelDashboard := libraryPanelDashboard{
|
||||
DashboardID: dashboardID,
|
||||
LibraryPanelID: panel.ID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: c.SignedInUser.UserId,
|
||||
}
|
||||
if _, err := session.Insert(&libraryPanelDashboard); err != nil {
|
||||
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
|
||||
return nil
|
||||
// connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard.
|
||||
func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error {
|
||||
err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
for _, uid := range uids {
|
||||
err := connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@ -110,6 +131,23 @@ func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid st
|
||||
})
|
||||
}
|
||||
|
||||
// disconnectLibraryPanelsForDashboard deletes connections for all Library Panels in a Dashboard.
|
||||
func (lps *LibraryPanelService) disconnectLibraryPanelsForDashboard(dashboardID int64, panelCount int64) error {
|
||||
return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if rowsAffected != panelCount {
|
||||
lps.log.Warn("Number of disconnects does not match number of panels", "dashboard", dashboardID, "rowsAffected", rowsAffected, "panelCount", panelCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanel, error) {
|
||||
libraryPanels := make([]LibraryPanel, 0)
|
||||
session.Table("library_panel")
|
||||
@ -183,6 +221,33 @@ func (lps *LibraryPanelService) getConnectedDashboards(c *models.ReqContext, uid
|
||||
return connectedDashboardIDs, err
|
||||
}
|
||||
|
||||
func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(dashboardID int64) (map[string]LibraryPanel, error) {
|
||||
libraryPanelMap := make(map[string]LibraryPanel)
|
||||
err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
sql := `SELECT
|
||||
lp.id, lp.org_id, lp.folder_id, lp.uid, lp.name, lp.model, lp.created, lp.created_by, lp.updated, updated_by
|
||||
FROM
|
||||
library_panel_dashboard AS lpd
|
||||
INNER JOIN
|
||||
library_panel AS lp ON lpd.librarypanel_id = lp.id AND lpd.dashboard_id=?`
|
||||
|
||||
var libraryPanels []LibraryPanel
|
||||
sess := session.SQL(sql, dashboardID)
|
||||
err := sess.Find(&libraryPanels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, panel := range libraryPanels {
|
||||
libraryPanelMap[panel.UID] = panel
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return libraryPanelMap, err
|
||||
}
|
||||
|
||||
// patchLibraryPanel updates a Library Panel.
|
||||
func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanel, error) {
|
||||
var libraryPanel LibraryPanel
|
||||
|
@ -1,8 +1,12 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@ -39,6 +43,157 @@ func (lps *LibraryPanelService) IsEnabled() bool {
|
||||
return lps.Cfg.IsPanelLibraryEnabled()
|
||||
}
|
||||
|
||||
// LoadLibraryPanelsForDashboard loops through all panels in dashboard JSON and replaces any library panel JSON
|
||||
// with JSON stored for library panel in db.
|
||||
func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(dash *models.Dashboard) error {
|
||||
if !lps.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
libraryPanels, err := lps.getLibraryPanelsForDashboardID(dash.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
panels := dash.Data.Get("panels").MustArray()
|
||||
for i, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
if libraryPanel.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
uid := libraryPanel.Get("uid").MustString()
|
||||
if len(uid) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
|
||||
libraryPanelInDB, ok := libraryPanels[uid]
|
||||
if !ok {
|
||||
return fmt.Errorf("found connection to library panel %q that isn't in database", uid)
|
||||
}
|
||||
|
||||
// we have a match between what is stored in db and in dashboard json
|
||||
libraryPanelModel, err := libraryPanelInDB.Model.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal library panel JSON: %w", err)
|
||||
}
|
||||
|
||||
libraryPanelModelAsJSON, err := simplejson.NewJson(libraryPanelModel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not convert library panel to simplejson model: %w", err)
|
||||
}
|
||||
|
||||
// set the library panel json as the new panel json in dashboard json
|
||||
dash.Data.Get("panels").SetIndex(i, libraryPanelModelAsJSON.Interface())
|
||||
|
||||
// set dashboard specific props
|
||||
elem := dash.Data.Get("panels").GetIndex(i)
|
||||
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
|
||||
elem.Set("id", panelAsJSON.Get("id").MustInt64())
|
||||
elem.Set("libraryPanel", map[string]interface{}{
|
||||
"uid": libraryPanelInDB.UID,
|
||||
"name": libraryPanelInDB.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanLibraryPanelsForDashboard loops through all panels in dashboard JSON and cleans up any library panel JSON so that
|
||||
// only the necessary JSON properties remain when storing the dashboard JSON.
|
||||
func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error {
|
||||
if !lps.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
panels := dash.Data.Get("panels").MustArray()
|
||||
for i, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
if libraryPanel.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
uid := libraryPanel.Get("uid").MustString()
|
||||
if len(uid) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
name := libraryPanel.Get("name").MustString()
|
||||
if len(name) == 0 {
|
||||
return errLibraryPanelHeaderNameMissing
|
||||
}
|
||||
|
||||
// keep only the necessary JSON properties, the rest of the properties should be safely stored in library_panels table
|
||||
gridPos := panelAsJSON.Get("gridPos").MustMap()
|
||||
id := panelAsJSON.Get("id").MustInt64(int64(i))
|
||||
dash.Data.Get("panels").SetIndex(i, map[string]interface{}{
|
||||
"id": id,
|
||||
"gridPos": gridPos,
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": uid,
|
||||
"name": name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
|
||||
func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
|
||||
if !lps.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
panels := dash.Data.Get("panels").MustArray()
|
||||
var libraryPanels []string
|
||||
for _, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
if libraryPanel.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
uid := libraryPanel.Get("uid").MustString()
|
||||
if len(uid) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
libraryPanels = append(libraryPanels, uid)
|
||||
}
|
||||
|
||||
return lps.connectLibraryPanelsForDashboard(c, libraryPanels, dash.Id)
|
||||
}
|
||||
|
||||
// DisconnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and disconnects any library panels from the dashboard.
|
||||
func (lps *LibraryPanelService) DisconnectLibraryPanelsForDashboard(dash *models.Dashboard) error {
|
||||
if !lps.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
panels := dash.Data.Get("panels").MustArray()
|
||||
panelCount := int64(0)
|
||||
for _, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
if libraryPanel.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
uid := libraryPanel.Get("uid").MustString()
|
||||
if len(uid) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
panelCount++
|
||||
}
|
||||
|
||||
return lps.disconnectLibraryPanelsForDashboard(dash.Id, panelCount)
|
||||
}
|
||||
|
||||
// AddMigration defines database migrations.
|
||||
// If Panel Library is not enabled does nothing.
|
||||
func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
|
||||
|
@ -2,9 +2,12 @@ package librarypanels
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaron.v1"
|
||||
@ -30,7 +33,7 @@ func TestCreateLibraryPanel(t *testing.T) {
|
||||
func TestConnectLibraryPanel(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to create a connection for a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"})
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
|
||||
response := sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, response.Status())
|
||||
})
|
||||
@ -45,7 +48,7 @@ func TestConnectLibraryPanel(t *testing.T) {
|
||||
err := json.Unmarshal(response.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"})
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
@ -97,7 +100,7 @@ func TestDeleteLibraryPanel(t *testing.T) {
|
||||
func TestDisconnectLibraryPanel(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to remove a connection with a library panel that does not exist, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"})
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"})
|
||||
response := sc.service.disconnectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, response.Status())
|
||||
})
|
||||
@ -112,7 +115,7 @@ func TestDisconnectLibraryPanel(t *testing.T) {
|
||||
err := json.Unmarshal(response.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"})
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.disconnectHandler(sc.reqContext)
|
||||
require.Equal(t, 404, response.Status())
|
||||
})
|
||||
@ -127,7 +130,7 @@ func TestDisconnectLibraryPanel(t *testing.T) {
|
||||
err := json.Unmarshal(response.Body(), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"})
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
response = sc.service.disconnectHandler(sc.reqContext)
|
||||
@ -330,7 +333,7 @@ func TestPatchLibraryPanel(t *testing.T) {
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"name": "Model - New name",
|
||||
"title": "Model - New name",
|
||||
"type": "text"
|
||||
}
|
||||
`),
|
||||
@ -344,7 +347,7 @@ func TestPatchLibraryPanel(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
existing.Result.FolderID = int64(2)
|
||||
existing.Result.Name = "Panel - New name"
|
||||
existing.Result.Model["name"] = "Model - New name"
|
||||
existing.Result.Model["title"] = "Model - New name"
|
||||
if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
@ -516,6 +519,578 @@ func TestPatchLibraryPanel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadLibraryPanelsForDashboard(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(&dash)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := simplejson.NewFromAny(expectedJSON)
|
||||
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(&dash)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to load a dashboard with a library panel that is not connected, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(&dash)
|
||||
require.EqualError(t, err, fmt.Errorf("found connection to library panel %q that isn't in database", existing.Result.UID).Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCleanLibraryPanelsForDashboard(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to store a dashboard with a library panel, it should just keep the correct JSON properties in library panel",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.CleanLibraryPanelsForDashboard(&dash)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := simplejson.NewFromAny(expectedJSON)
|
||||
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.CleanLibraryPanelsForDashboard(&dash)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to store a dashboard with a library panel without name, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: 1,
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.CleanLibraryPanelsForDashboard(&dash)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderNameMissing.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectLibraryPanelsForDashboard(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to store a dashboard with a library panel, it should connect the two",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: int64(1),
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID})
|
||||
response = sc.service.getConnectedDashboardsHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var dashResult libraryPanelDashboardsResult
|
||||
err = json.Unmarshal(response.Body(), &dashResult)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dashResult.Result, 1)
|
||||
require.Equal(t, int64(1), dashResult.Result[0])
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: int64(1),
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisconnectLibraryPanelsForDashboard(t *testing.T) {
|
||||
testScenario(t, "When an admin tries to delete a dashboard with a library panel, it should disconnect the two",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": existing.Result.UID,
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: int64(1),
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.DisconnectLibraryPanelsForDashboard(&dash)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID})
|
||||
response = sc.service.getConnectedDashboardsHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var dashResult libraryPanelDashboardsResult
|
||||
err = json.Unmarshal(response.Body(), &dashResult)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, dashResult.Result)
|
||||
})
|
||||
|
||||
testScenario(t, "When an admin tries to delete a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
command := getCreateCommand(1, "Text - Library Panel1")
|
||||
response := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
var existing libraryPanelResult
|
||||
err := json.Unmarshal(response.Body(), &existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"})
|
||||
response = sc.service.connectHandler(sc.reqContext)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
dashJSON := map[string]interface{}{
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(2),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"name": existing.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Id: int64(1),
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
|
||||
err = sc.service.DisconnectLibraryPanelsForDashboard(&dash)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
}
|
||||
|
||||
type libraryPanel struct {
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"orgId"`
|
||||
@ -570,7 +1145,7 @@ func getCreateCommand(folderID int64, name string) createLibraryPanelCommand {
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"name": "Text - Library Panel",
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text"
|
||||
}
|
||||
`),
|
||||
|
@ -40,6 +40,10 @@ var (
|
||||
errLibraryPanelNotFound = errors.New("library panel could not be found")
|
||||
// errLibraryPanelDashboardNotFound is an error for when a library panel connection can't be found.
|
||||
errLibraryPanelDashboardNotFound = errors.New("library panel connection could not be found")
|
||||
// errLibraryPanelHeaderUIDMissing is an error for when a library panel header is missing the uid property.
|
||||
errLibraryPanelHeaderUIDMissing = errors.New("library panel header is missing required property uid")
|
||||
// errLibraryPanelHeaderNameMissing is an error for when a library panel header is missing the name property.
|
||||
errLibraryPanelHeaderNameMissing = errors.New("library panel header is missing required property name")
|
||||
)
|
||||
|
||||
// Commands
|
||||
|
Loading…
Reference in New Issue
Block a user