Commit b7b6632a by Hugo Häggmark Committed by GitHub

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>
parent 36dc70e1
{
"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"
......@@ -79,6 +80,7 @@ type HTTPServer struct {
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,10 +42,8 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre
return libraryPanel, err
}
// 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)
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
}
......@@ -54,15 +54,36 @@ func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid strin
DashboardID: dashboardID,
LibraryPanelID: panel.ID,
Created: time.Now(),
CreatedBy: c.SignedInUser.UserId,
CreatedBy: user.UserId,
}
if _, err := session.Insert(&libraryPanelDashboard); err != nil {
if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
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 {
return connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID)
})
return err
}
// 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 nil
})
return err
......@@ -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
......
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) {
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment