mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Load library panels in the frontend rather than the backend (#50560)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -3225,10 +3225,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"]
|
||||
],
|
||||
"public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -3241,13 +3240,11 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@@ -3701,8 +3698,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "26"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "29"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "28"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "31"]
|
||||
],
|
||||
"public/app/features/dashboard/state/TimeModel.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface PanelModel<TOptions = any, TCustomFieldConfig = any> {
|
||||
/** ID of the panel within the current dashboard */
|
||||
id: number;
|
||||
|
||||
/** The panel type */
|
||||
type: string;
|
||||
|
||||
/** Panel title */
|
||||
title?: string;
|
||||
|
||||
|
||||
@@ -214,12 +214,6 @@ 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)
|
||||
|
||||
// load library panels JSON for this dashboard
|
||||
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c.Req.Context(), dash)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while loading library panels", err)
|
||||
}
|
||||
|
||||
if hs.QueryLibraryService != nil && !hs.QueryLibraryService.IsDisabled() {
|
||||
if err := hs.QueryLibraryService.UpdateDashboardQueries(c.Req.Context(), c.SignedInUser, dash); err != nil {
|
||||
return response.Error(500, "Error while loading saved queries", err)
|
||||
@@ -431,12 +425,6 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -1241,14 +1241,6 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard
|
||||
type mockLibraryPanelService struct {
|
||||
}
|
||||
|
||||
func (m *mockLibraryPanelService) LoadLibraryPanelsForDashboard(c context.Context, dash *models.Dashboard) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -61,18 +61,19 @@ type LibraryElementWithMeta struct {
|
||||
|
||||
// LibraryElementDTO is the frontend DTO for entities.
|
||||
type LibraryElementDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"orgId"`
|
||||
FolderID int64 `json:"folderId"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Kind int64 `json:"kind"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Model json.RawMessage `json:"model"`
|
||||
Version int64 `json:"version"`
|
||||
Meta LibraryElementDTOMeta `json:"meta"`
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"orgId"`
|
||||
FolderID int64 `json:"folderId"`
|
||||
FolderUID string `json:"folderUid"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Kind int64 `json:"kind"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Model json.RawMessage `json:"model"`
|
||||
Version int64 `json:"version"`
|
||||
Meta LibraryElementDTOMeta `json:"meta"`
|
||||
SchemaVersion int64 `json:"schemaVersion,omitempty"`
|
||||
}
|
||||
|
||||
// LibraryElementSearchResult is the search result for entities.
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -29,8 +28,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
|
||||
|
||||
// Service is a service for operating on library panels.
|
||||
type Service interface {
|
||||
LoadLibraryPanelsForDashboard(c context.Context, dash *models.Dashboard) error
|
||||
CleanLibraryPanelsForDashboard(dash *models.Dashboard) error
|
||||
ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error
|
||||
ImportLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error
|
||||
}
|
||||
@@ -49,160 +46,6 @@ type LibraryPanelService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// 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(c context.Context, dash *models.Dashboard) error {
|
||||
elements, err := lps.LibraryElementService.GetElementsForDashboard(c, dash.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return loadLibraryPanelsRecursively(elements, dash.Data)
|
||||
}
|
||||
|
||||
func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryElementDTO, parent *simplejson.Json) error {
|
||||
panels := parent.Get("panels").MustArray()
|
||||
for i, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
panelType := panelAsJSON.Get("type").MustString()
|
||||
if !isLibraryPanelOrRow(libraryPanel, panelType) {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a row
|
||||
if panelType == "row" {
|
||||
err := loadLibraryPanelsRecursively(elements, panelAsJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
UID := libraryPanel.Get("uid").MustString()
|
||||
if len(UID) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
|
||||
elementInDB, ok := elements[UID]
|
||||
if !ok {
|
||||
elem := parent.Get("panels").GetIndex(i)
|
||||
gridPos := panelAsJSON.Get("gridPos").MustMap()
|
||||
if gridPos == nil {
|
||||
elem.Del("gridPos")
|
||||
} else {
|
||||
elem.Set("gridPos", gridPos)
|
||||
}
|
||||
elem.Set("id", panelAsJSON.Get("id").MustInt64())
|
||||
elem.Set("libraryPanel", map[string]interface{}{
|
||||
"uid": UID,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if models.LibraryElementKind(elementInDB.Kind) != models.PanelElement {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a match between what is stored in db and in dashboard json
|
||||
libraryPanelModel, err := elementInDB.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
|
||||
parent.Get("panels").SetIndex(i, libraryPanelModelAsJSON.Interface())
|
||||
|
||||
// set dashboard specific props
|
||||
elem := parent.Get("panels").GetIndex(i)
|
||||
gridPos := panelAsJSON.Get("gridPos").MustMap()
|
||||
if gridPos == nil {
|
||||
elem.Del("gridPos")
|
||||
} else {
|
||||
elem.Set("gridPos", gridPos)
|
||||
}
|
||||
elem.Set("id", panelAsJSON.Get("id").MustInt64())
|
||||
elem.Set("libraryPanel", map[string]interface{}{
|
||||
"uid": elementInDB.UID,
|
||||
"name": elementInDB.Name,
|
||||
"type": elementInDB.Type,
|
||||
"description": elementInDB.Description,
|
||||
"version": elementInDB.Version,
|
||||
"meta": map[string]interface{}{
|
||||
"folderName": elementInDB.Meta.FolderName,
|
||||
"folderUid": elementInDB.Meta.FolderUID,
|
||||
"connectedDashboards": elementInDB.Meta.ConnectedDashboards,
|
||||
"created": elementInDB.Meta.Created,
|
||||
"updated": elementInDB.Meta.Updated,
|
||||
"createdBy": map[string]interface{}{
|
||||
"id": elementInDB.Meta.CreatedBy.ID,
|
||||
"name": elementInDB.Meta.CreatedBy.Name,
|
||||
"avatarUrl": elementInDB.Meta.CreatedBy.AvatarURL,
|
||||
},
|
||||
"updatedBy": map[string]interface{}{
|
||||
"id": elementInDB.Meta.UpdatedBy.ID,
|
||||
"name": elementInDB.Meta.UpdatedBy.Name,
|
||||
"avatarUrl": elementInDB.Meta.UpdatedBy.AvatarURL,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
return cleanLibraryPanelsRecursively(dash.Data)
|
||||
}
|
||||
|
||||
func cleanLibraryPanelsRecursively(parent *simplejson.Json) error {
|
||||
panels := parent.Get("panels").MustArray()
|
||||
for i, panel := range panels {
|
||||
panelAsJSON := simplejson.NewFromAny(panel)
|
||||
libraryPanel := panelAsJSON.Get("libraryPanel")
|
||||
panelType := panelAsJSON.Get("type").MustString()
|
||||
if !isLibraryPanelOrRow(libraryPanel, panelType) {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a row
|
||||
if panelType == "row" {
|
||||
err := cleanLibraryPanelsRecursively(panelAsJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// we have a library panel
|
||||
UID := libraryPanel.Get("uid").MustString()
|
||||
if len(UID) == 0 {
|
||||
return errLibraryPanelHeaderUIDMissing
|
||||
}
|
||||
|
||||
// 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))
|
||||
parent.Get("panels").SetIndex(i, map[string]interface{}{
|
||||
"id": ID,
|
||||
"gridPos": gridPos,
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": UID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
|
||||
func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error {
|
||||
panels := dash.Data.Get("panels").MustArray()
|
||||
|
||||
@@ -35,697 +35,6 @@ import (
|
||||
const userInDbName = "user_in_db"
|
||||
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
|
||||
|
||||
func TestLoadLibraryPanelsForDashboard(t *testing.T) {
|
||||
scenarioWithLibraryPanel(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) {
|
||||
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": sc.initialResult.Result.UID,
|
||||
"name": sc.initialResult.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing LoadLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
err := sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"title": "Testing LoadLibraryPanelsForDashboard",
|
||||
"uid": dashInDB.Uid,
|
||||
"version": dashInDB.Version,
|
||||
"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}",
|
||||
"description": "A description",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
"name": sc.initialResult.Result.Name,
|
||||
"type": sc.initialResult.Result.Type,
|
||||
"description": sc.initialResult.Result.Description,
|
||||
"version": sc.initialResult.Result.Version,
|
||||
"meta": map[string]interface{}{
|
||||
"folderName": "ScenarioFolder",
|
||||
"folderUid": sc.folder.Uid,
|
||||
"connectedDashboards": int64(1),
|
||||
"created": sc.initialResult.Result.Meta.Created,
|
||||
"updated": sc.initialResult.Result.Meta.Updated,
|
||||
"createdBy": map[string]interface{}{
|
||||
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
"updatedBy": map[string]interface{}{
|
||||
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
"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)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with library panels inside and outside of rows, it should copy JSON properties from library panels",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := libraryelements.CreateLibraryElementCommand{
|
||||
FolderID: sc.initialResult.Result.FolderID,
|
||||
Name: "Outside row",
|
||||
Model: []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "A description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
|
||||
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{}{
|
||||
"collapsed": true,
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
"id": int64(2),
|
||||
"type": "row",
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(3),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Inside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(5),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 19,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": outsidePanel.UID,
|
||||
},
|
||||
"title": "Outside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing LoadLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{outsidePanel.UID, sc.initialResult.Result.UID}, dashInDB.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"title": "Testing LoadLibraryPanelsForDashboard",
|
||||
"uid": dashInDB.Uid,
|
||||
"version": dashInDB.Version,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"collapsed": true,
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
"id": int64(2),
|
||||
"type": "row",
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(3),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
"name": sc.initialResult.Result.Name,
|
||||
"type": sc.initialResult.Result.Type,
|
||||
"description": sc.initialResult.Result.Description,
|
||||
"version": sc.initialResult.Result.Version,
|
||||
"meta": map[string]interface{}{
|
||||
"folderName": "ScenarioFolder",
|
||||
"folderUid": sc.folder.Uid,
|
||||
"connectedDashboards": int64(1),
|
||||
"created": sc.initialResult.Result.Meta.Created,
|
||||
"updated": sc.initialResult.Result.Meta.Updated,
|
||||
"createdBy": map[string]interface{}{
|
||||
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
"updatedBy": map[string]interface{}{
|
||||
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(5),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 19,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"description": "A description",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": outsidePanel.UID,
|
||||
"name": outsidePanel.Name,
|
||||
"type": outsidePanel.Type,
|
||||
"description": outsidePanel.Description,
|
||||
"version": outsidePanel.Version,
|
||||
"meta": map[string]interface{}{
|
||||
"folderName": "ScenarioFolder",
|
||||
"folderUid": sc.folder.Uid,
|
||||
"connectedDashboards": int64(1),
|
||||
"created": outsidePanel.Meta.Created,
|
||||
"updated": outsidePanel.Meta.Updated,
|
||||
"createdBy": map[string]interface{}{
|
||||
"id": outsidePanel.Meta.CreatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
"updatedBy": map[string]interface{}{
|
||||
"id": outsidePanel.Meta.UpdatedBy.ID,
|
||||
"name": userInDbName,
|
||||
"avatarUrl": userInDbAvatar,
|
||||
},
|
||||
},
|
||||
},
|
||||
"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)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
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": sc.initialResult.Result.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing LoadLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
err := sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel that is not connected, it should set correct JSON and continue",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
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": sc.initialResult.Result.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing LoadLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
|
||||
err := sc.service.LoadLibraryPanelsForDashboard(sc.ctx, dashInDB)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"title": "Testing LoadLibraryPanelsForDashboard",
|
||||
"uid": dashInDB.Uid,
|
||||
"version": dashInDB.Version,
|
||||
"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": sc.initialResult.Result.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := simplejson.NewFromAny(expectedJSON)
|
||||
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCleanLibraryPanelsForDashboard(t *testing.T) {
|
||||
scenarioWithLibraryPanel(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) {
|
||||
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": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing CleanLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
|
||||
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"title": "Testing CleanLibraryPanelsForDashboard",
|
||||
"uid": dashInDB.Uid,
|
||||
"version": dashInDB.Version,
|
||||
"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": sc.initialResult.Result.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := simplejson.NewFromAny(expectedJSON)
|
||||
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should just keep the correct JSON properties",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
cmd := libraryelements.CreateLibraryElementCommand{
|
||||
FolderID: sc.initialResult.Result.FolderID,
|
||||
Name: "Outside row",
|
||||
Model: []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
"description": "A description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd)
|
||||
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{}{
|
||||
"collapsed": true,
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
"id": int64(2),
|
||||
"type": "row",
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(3),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
"title": "Inside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(5),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 19,
|
||||
},
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": outsidePanel.UID,
|
||||
},
|
||||
"title": "Outside row",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing CleanLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
|
||||
err = sc.service.CleanLibraryPanelsForDashboard(dashInDB)
|
||||
require.NoError(t, err)
|
||||
expectedJSON := map[string]interface{}{
|
||||
"title": "Testing CleanLibraryPanelsForDashboard",
|
||||
"uid": dashInDB.Uid,
|
||||
"version": dashInDB.Version,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(1),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"collapsed": true,
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
"id": int64(2),
|
||||
"type": "row",
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": int64(3),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(4),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": sc.initialResult.Result.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": int64(5),
|
||||
"gridPos": map[string]interface{}{
|
||||
"h": 6,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 19,
|
||||
},
|
||||
"libraryPanel": map[string]interface{}{
|
||||
"uid": outsidePanel.UID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := simplejson.NewFromAny(expectedJSON)
|
||||
if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
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": sc.initialResult.Result.Name,
|
||||
},
|
||||
"title": "Text - Library Panel",
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
dash := models.Dashboard{
|
||||
Title: "Testing CleanLibraryPanelsForDashboard",
|
||||
Data: simplejson.NewFromAny(dashJSON),
|
||||
}
|
||||
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
|
||||
|
||||
err := sc.service.CleanLibraryPanelsForDashboard(dashInDB)
|
||||
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnectLibraryPanelsForDashboard(t *testing.T) {
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should connect the two",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
LibraryPanelsSearchVariant,
|
||||
} from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
|
||||
import { LibraryElementDTO } from '../../../library-panels/types';
|
||||
import { toPanelModelLibraryPanel } from '../../../library-panels/utils';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
|
||||
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
|
||||
@@ -116,7 +115,7 @@ export const AddPanelWidgetUnconnected = ({ panel, dashboard }: Props) => {
|
||||
const newPanel: PanelModel = {
|
||||
...panelInfo.model,
|
||||
gridPos,
|
||||
libraryPanel: toPanelModelLibraryPanel(panelInfo),
|
||||
libraryPanel: panelInfo,
|
||||
};
|
||||
|
||||
dashboard.addPanel(newPanel);
|
||||
|
||||
@@ -40,6 +40,20 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/features/library-panels/state/api', () => ({
|
||||
getLibraryPanel: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
model: {
|
||||
type: 'graph',
|
||||
datasource: {
|
||||
type: 'testdb',
|
||||
uid: '${DS_GFDB}',
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
variableAdapters.register(createQueryVariableAdapter());
|
||||
variableAdapters.register(createConstantVariableAdapter());
|
||||
variableAdapters.register(createDataSourceVariableAdapter());
|
||||
@@ -146,8 +160,6 @@ describe('given dashboard with repeated panels', () => {
|
||||
{ id: 9, datasource: { uid: '$ds', type: 'other2' } },
|
||||
{
|
||||
id: 17,
|
||||
datasource: { uid: '$ds', type: 'other2' },
|
||||
type: 'graph',
|
||||
libraryPanel: {
|
||||
name: 'Library Panel 2',
|
||||
uid: 'ah8NqyDPs',
|
||||
@@ -181,8 +193,8 @@ describe('given dashboard with repeated panels', () => {
|
||||
{ id: 15, repeat: null, repeatPanelId: 14 },
|
||||
{
|
||||
id: 16,
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
type: 'graph',
|
||||
// datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
// type: 'graph',
|
||||
libraryPanel: {
|
||||
name: 'Library Panel',
|
||||
uid: 'jL6MrxCMz',
|
||||
@@ -218,6 +230,18 @@ describe('given dashboard with repeated panels', () => {
|
||||
} as PanelPluginMeta;
|
||||
|
||||
dash = new DashboardModel(dash, {}, () => dash.templating.list);
|
||||
|
||||
// init library panels
|
||||
dash.getPanelById(17).initLibraryPanel({
|
||||
uid: 'ah8NqyDPs',
|
||||
name: 'Library Panel 2',
|
||||
model: {
|
||||
datasource: { type: 'other2', uid: '$ds' },
|
||||
targets: [{ refId: 'A', datasource: { type: 'other2', uid: '$ds' } }],
|
||||
type: 'graph',
|
||||
},
|
||||
});
|
||||
|
||||
const exporter = new DashboardExporter();
|
||||
exporter.makeExportable(dash).then((clean) => {
|
||||
exported = clean;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DataSourceRef, PanelPluginMeta } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
|
||||
import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
|
||||
import { LibraryElementKind } from '../../../library-panels/types';
|
||||
@@ -167,10 +168,15 @@ export class DashboardExporter {
|
||||
}
|
||||
};
|
||||
|
||||
const processLibraryPanels = (panel: any) => {
|
||||
const processLibraryPanels = async (panel: PanelModel) => {
|
||||
if (isPanelModelLibraryPanel(panel)) {
|
||||
const { libraryPanel, ...model } = panel;
|
||||
const { name, uid } = libraryPanel;
|
||||
const { name, uid } = panel.libraryPanel;
|
||||
let model = panel.libraryPanel.model;
|
||||
if (!model) {
|
||||
const libPanel = await getLibraryPanel(uid, true);
|
||||
model = libPanel.model;
|
||||
}
|
||||
|
||||
const { gridPos, id, ...rest } = model;
|
||||
if (!libraryPanels.has(uid)) {
|
||||
libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model: rest });
|
||||
@@ -221,11 +227,11 @@ export class DashboardExporter {
|
||||
|
||||
// we need to process all panels again after all the promises are resolved
|
||||
// so all data sources, variables and targets have been templateized when we process library panels
|
||||
for (const panel of saveModel.panels) {
|
||||
processLibraryPanels(panel);
|
||||
for (const panel of dashboard.panels) {
|
||||
await processLibraryPanels(panel);
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
processLibraryPanels(rowPanel);
|
||||
await processLibraryPanels(rowPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function ignoreChanges(current: DashboardModel, original: object | null)
|
||||
/**
|
||||
* Remove stuff that should not count in diff
|
||||
*/
|
||||
function cleanDashboardFromIgnoredChanges(dashData: any) {
|
||||
function cleanDashboardFromIgnoredChanges(dashData: unknown) {
|
||||
// need to new up the domain model class to get access to expand / collapse row logic
|
||||
const model = new DashboardModel(dashData);
|
||||
|
||||
@@ -193,7 +193,7 @@ function cleanDashboardFromIgnoredChanges(dashData: any) {
|
||||
return dash;
|
||||
}
|
||||
|
||||
export function hasChanges(current: DashboardModel, original: any) {
|
||||
export function hasChanges(current: DashboardModel, original: unknown) {
|
||||
if (current.hasUnsavedChanges()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,12 +69,15 @@ export class DashboardPanelUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isViewing, isEditing, width, height, lazy, plugin } = this.props;
|
||||
renderPanel = (isInView: boolean) => {
|
||||
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
|
||||
|
||||
const renderPanelChrome = (isInView: boolean) =>
|
||||
plugin &&
|
||||
(plugin.angularPanelCtrl ? (
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (plugin && plugin.angularPanelCtrl) {
|
||||
return (
|
||||
<PanelChromeAngular
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
@@ -85,26 +88,33 @@ export class DashboardPanelUnconnected extends PureComponent<Props> {
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
) : (
|
||||
<PanelStateWrapper
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isViewing={isViewing}
|
||||
isEditing={isEditing}
|
||||
isInView={isInView}
|
||||
width={width}
|
||||
height={height}
|
||||
onInstanceStateChange={this.onInstanceStateChange}
|
||||
/>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelStateWrapper
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isViewing={isViewing}
|
||||
isEditing={isEditing}
|
||||
isInView={isInView}
|
||||
width={width}
|
||||
height={height}
|
||||
onInstanceStateChange={this.onInstanceStateChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, height, lazy } = this.props;
|
||||
|
||||
return lazy ? (
|
||||
<LazyLoader width={width} height={height} onChange={this.onVisibilityChange} onLoad={this.onPanelLoad}>
|
||||
{({ isInView }) => renderPanelChrome(isInView)}
|
||||
{this.renderPanel}
|
||||
</LazyLoader>
|
||||
) : (
|
||||
renderPanelChrome(true)
|
||||
this.renderPanel(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +278,20 @@ export class DashboardModel implements TimeModel {
|
||||
this.isSnapshotTruthy() || !(panel.type === 'add-panel' || panel.repeatPanelId || panel.repeatedByRow)
|
||||
)
|
||||
.map((panel) => {
|
||||
// Clean libarary panels on save
|
||||
if (panel.libraryPanel) {
|
||||
const { id, title, libraryPanel, gridPos } = panel;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
gridPos,
|
||||
libraryPanel: {
|
||||
uid: libraryPanel.uid,
|
||||
name: libraryPanel.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// If we save while editing we should include the panel in edit mode instead of the
|
||||
// unmodified source panel
|
||||
if (this.panelInEdit && this.panelInEdit.id === panel.id) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
RenderEvent,
|
||||
} from 'app/types/events';
|
||||
|
||||
import { PanelModelLibraryPanel } from '../../library-panels/types';
|
||||
import { LibraryElementDTO, LibraryPanelRef } from '../../library-panels/types';
|
||||
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
|
||||
import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl';
|
||||
import { getTimeSrv } from '../services/TimeSrv';
|
||||
@@ -171,7 +171,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
links?: DataLink[];
|
||||
declare transparent: boolean;
|
||||
|
||||
libraryPanel?: { uid: undefined; name: string; version?: number } | PanelModelLibraryPanel;
|
||||
libraryPanel?: LibraryPanelRef | LibraryElementDTO;
|
||||
|
||||
autoMigrateFrom?: string;
|
||||
|
||||
@@ -659,6 +659,19 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
getDisplayTitle(): string {
|
||||
return this.replaceVariables(this.title, undefined, 'text');
|
||||
}
|
||||
|
||||
initLibraryPanel(libPanel: LibraryElementDTO) {
|
||||
for (const [key, val] of Object.entries(libPanel.model)) {
|
||||
switch (key) {
|
||||
case 'id':
|
||||
case 'gridPos':
|
||||
case 'libraryPanel': // recursive?
|
||||
continue;
|
||||
}
|
||||
(this as any)[key] = val; // :grimmice:
|
||||
}
|
||||
this.libraryPanel = libPanel;
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginVersion(plugin: PanelPlugin): string {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
|
||||
|
||||
const { saveLibraryPanel } = usePanelSave();
|
||||
const onCreate = useCallback(() => {
|
||||
panel.libraryPanel = { uid: undefined, name: panelName };
|
||||
panel.libraryPanel = { uid: '', name: panelName };
|
||||
saveLibraryPanel(panel, folderId!).then((res) => {
|
||||
if (!(res instanceof Error)) {
|
||||
onDismiss();
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import { DateTimeInput, GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
|
||||
import { isPanelModelLibraryPanel } from '../../guard';
|
||||
import { PanelModelWithLibraryPanel } from '../../types';
|
||||
|
||||
interface Props {
|
||||
@@ -15,28 +14,29 @@ interface Props {
|
||||
export const LibraryPanelInformation = ({ panel, formatDate }: Props) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
if (!isPanelModelLibraryPanel(panel)) {
|
||||
const meta = panel.libraryPanel?.meta;
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<div className={styles.libraryPanelInfo}>
|
||||
{`Used on ${panel.libraryPanel.meta.connectedDashboards} `}
|
||||
{panel.libraryPanel.meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}
|
||||
{`Used on ${meta.connectedDashboards} `}
|
||||
{meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}
|
||||
</div>
|
||||
<div className={styles.libraryPanelInfo}>
|
||||
Last edited on {formatDate?.(panel.libraryPanel.meta.updated, 'L') ?? panel.libraryPanel.meta.updated} by
|
||||
{panel.libraryPanel.meta.updatedBy.avatarUrl && (
|
||||
Last edited on {formatDate?.(meta.updated, 'L') ?? meta.updated} by
|
||||
{meta.updatedBy.avatarUrl && (
|
||||
<img
|
||||
width="22"
|
||||
height="22"
|
||||
className={styles.userAvatar}
|
||||
src={panel.libraryPanel.meta.updatedBy.avatarUrl}
|
||||
alt={`Avatar for ${panel.libraryPanel.meta.updatedBy.name}`}
|
||||
src={meta.updatedBy.avatarUrl}
|
||||
alt={`Avatar for ${meta.updatedBy.name}`}
|
||||
/>
|
||||
)}
|
||||
{panel.libraryPanel.meta.updatedBy.name}
|
||||
{meta.updatedBy.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||
import { DashboardSearchItem } from '../../search/types';
|
||||
import {
|
||||
@@ -54,7 +56,18 @@ export async function getLibraryPanel(uid: string, isHandled = false): Promise<L
|
||||
showErrorAlert: !isHandled,
|
||||
})
|
||||
);
|
||||
return response.data.result;
|
||||
// kinda heavy weight migration process!!!
|
||||
const { result } = response.data;
|
||||
const dash = new DashboardModel({
|
||||
schemaVersion: 35, // should be saved in the library panel
|
||||
panels: [result.model],
|
||||
});
|
||||
const model = dash.panels[0].getSaveModel(); // migrated panel
|
||||
dash.destroy(); // kill event listeners
|
||||
return {
|
||||
...result,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLibraryPanelByName(name: string): Promise<LibraryElementDTO[]> {
|
||||
@@ -76,9 +89,9 @@ export async function addLibraryPanel(
|
||||
}
|
||||
|
||||
export async function updateLibraryPanel(panelSaveModel: PanelModelWithLibraryPanel): Promise<LibraryElementDTO> {
|
||||
const { uid, name, version } = panelSaveModel.libraryPanel;
|
||||
const { libraryPanel, ...model } = panelSaveModel;
|
||||
const { uid, name, version } = libraryPanel;
|
||||
const kind = LibraryElementKind.Panel;
|
||||
const model = panelSaveModel;
|
||||
const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, {
|
||||
name,
|
||||
model,
|
||||
|
||||
@@ -59,10 +59,13 @@ export interface LibraryElementDTOMetaUser {
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export type PanelModelLibraryPanel = Pick<LibraryElementDTO, 'uid' | 'name' | 'meta' | 'version'>;
|
||||
export interface LibraryPanelRef {
|
||||
name: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface PanelModelWithLibraryPanel extends PanelModel {
|
||||
libraryPanel: PanelModelLibraryPanel;
|
||||
libraryPanel: LibraryElementDTO;
|
||||
}
|
||||
|
||||
export type DispatchResult = (dispatch: Dispatch<AnyAction>) => void;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AppNotification } from '../../types';
|
||||
import { PanelModel } from '../dashboard/state';
|
||||
|
||||
import { addLibraryPanel, updateLibraryPanel } from './state/api';
|
||||
import { LibraryElementDTO, PanelModelLibraryPanel } from './types';
|
||||
import { LibraryElementDTO } from './types';
|
||||
|
||||
export function createPanelLibraryErrorNotification(message: string): AppNotification {
|
||||
return createErrorNotification(message);
|
||||
@@ -13,11 +13,6 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif
|
||||
return createSuccessNotification(message);
|
||||
}
|
||||
|
||||
export function toPanelModelLibraryPanel(libraryPanelDto: LibraryElementDTO): PanelModelLibraryPanel {
|
||||
const { uid, name, meta, version } = libraryPanelDto;
|
||||
return { uid, name, meta, version };
|
||||
}
|
||||
|
||||
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryElementDTO> {
|
||||
const panelSaveModel = toPanelSaveModel(panel);
|
||||
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId);
|
||||
@@ -42,7 +37,7 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDT
|
||||
panel.restoreModel({
|
||||
...updated.model,
|
||||
configRev: 0, // reset config rev, since changes have been saved
|
||||
libraryPanel: toPanelModelLibraryPanel(updated),
|
||||
libraryPanel: updated,
|
||||
title: panel.title,
|
||||
});
|
||||
panel.hasSavedPanelEditChange = true;
|
||||
@@ -54,7 +49,7 @@ function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<Library
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (panel.libraryPanel && panel.libraryPanel.uid === undefined) {
|
||||
if (panel.libraryPanel && panel.libraryPanel.uid === '') {
|
||||
return addLibraryPanel(panel, folderId!);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DataTransformerConfig, FieldConfigSource } from '@grafana/data';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { getPanelOptionsWithDefaults } from 'app/features/dashboard/state/getPanelOptionsWithDefaults';
|
||||
import { getLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
import { LibraryElementDTO } from 'app/features/library-panels/types';
|
||||
import { toPanelModelLibraryPanel } from 'app/features/library-panels/utils';
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions';
|
||||
import { ThunkResult } from 'app/types';
|
||||
@@ -18,6 +18,12 @@ import {
|
||||
|
||||
export function initPanelState(panel: PanelModel): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) {
|
||||
// this will call init with a loaded libary panel if it loads succesfully
|
||||
dispatch(loadLibraryPanelAndUpdate(panel));
|
||||
return;
|
||||
}
|
||||
|
||||
let pluginToLoad = panel.type;
|
||||
let plugin = getStore().plugins.panels[pluginToLoad];
|
||||
|
||||
@@ -118,7 +124,7 @@ export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryEle
|
||||
...libraryPanel.model,
|
||||
gridPos: panel.gridPos,
|
||||
id: panel.id,
|
||||
libraryPanel: toPanelModelLibraryPanel(libraryPanel),
|
||||
libraryPanel: libraryPanel,
|
||||
});
|
||||
|
||||
// a new library panel usually means new queries, clear any current result
|
||||
@@ -155,3 +161,22 @@ export function changeToLibraryPanel(panel: PanelModel, libraryPanel: LibraryEle
|
||||
panel.events.publish(PanelOptionsChangedEvent);
|
||||
};
|
||||
}
|
||||
|
||||
export function loadLibraryPanelAndUpdate(panel: PanelModel): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const uid = panel.libraryPanel!.uid!;
|
||||
try {
|
||||
const libPanel = await getLibraryPanel(uid, true);
|
||||
panel.initLibraryPanel(libPanel);
|
||||
dispatch(initPanelState(panel));
|
||||
} catch (ex) {
|
||||
console.log('ERROR: ', ex);
|
||||
dispatch(
|
||||
panelModelAndPluginReady({
|
||||
key: panel.key,
|
||||
plugin: getPanelPluginNotFound('Unable to load library panel: ' + uid, false),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('textPanelMigrationHandler', () => {
|
||||
it('then should just pass options through', () => {
|
||||
const panel: PanelModel<PanelOptions> = {
|
||||
id: 1,
|
||||
type: 'text',
|
||||
fieldConfig: {} as unknown as FieldConfigSource,
|
||||
options: {
|
||||
content: `# Title
|
||||
@@ -65,6 +66,7 @@ describe('textPanelMigrationHandler', () => {
|
||||
const mode = 'text' as unknown as TextMode;
|
||||
const panel: PanelModel<PanelOptions> = {
|
||||
id: 1,
|
||||
type: 'text',
|
||||
fieldConfig: {} as unknown as FieldConfigSource,
|
||||
options: {
|
||||
content: `# Title
|
||||
|
||||
Reference in New Issue
Block a user