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:
co-authored by
Arve Knudsen
parent
36dc70e168
commit
b7b6632a4d
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user