diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 480f880b984..a8fc3c60897 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -47,11 +47,25 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte return err } - panels := dash.Data.Get("panels").MustArray() + 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") - if libraryPanel.Interface() == nil { + 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 } @@ -64,7 +78,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte elementInDB, ok := elements[uid] if !ok { name := libraryPanel.Get("name").MustString() - elem := dash.Data.Get("panels").GetIndex(i) + elem := parent.Get("panels").GetIndex(i) elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap()) elem.Set("id", panelAsJSON.Get("id").MustInt64()) elem.Set("type", fmt.Sprintf("Name: \"%s\", UID: \"%s\"", name, uid)) @@ -91,10 +105,10 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte } // set the library panel json as the new panel json in dashboard json - dash.Data.Get("panels").SetIndex(i, libraryPanelModelAsJSON.Interface()) + parent.Get("panels").SetIndex(i, libraryPanelModelAsJSON.Interface()) // set dashboard specific props - elem := dash.Data.Get("panels").GetIndex(i) + elem := parent.Get("panels").GetIndex(i) elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap()) elem.Set("id", panelAsJSON.Get("id").MustInt64()) elem.Set("libraryPanel", map[string]interface{}{ @@ -129,11 +143,25 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte // 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 { - panels := dash.Data.Get("panels").MustArray() + 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") - if libraryPanel.Interface() == nil { + 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 } @@ -150,7 +178,7 @@ func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dash // 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{}{ + parent.Get("panels").SetIndex(i, map[string]interface{}{ "id": id, "gridPos": gridPos, "libraryPanel": map[string]interface{}{ @@ -167,10 +195,39 @@ func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dash func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error { panels := dash.Data.Get("panels").MustArray() libraryPanels := make(map[string]string) + err := connectLibraryPanelsRecursively(c, panels, libraryPanels) + if err != nil { + return err + } + + elementUIDs := make([]string, 0, len(libraryPanels)) + for libraryPanel := range libraryPanels { + elementUIDs = append(elementUIDs, libraryPanel) + } + + return lps.LibraryElementService.ConnectElementsToDashboard(c, elementUIDs, dash.Id) +} + +func isLibraryPanelOrRow(panel *simplejson.Json, panelType string) bool { + return panel.Interface() != nil || panelType == "row" +} + +func connectLibraryPanelsRecursively(c *models.ReqContext, panels []interface{}, libraryPanels map[string]string) error { for _, panel := range panels { panelAsJSON := simplejson.NewFromAny(panel) libraryPanel := panelAsJSON.Get("libraryPanel") - if libraryPanel.Interface() == nil { + panelType := panelAsJSON.Get("type").MustString() + if !isLibraryPanelOrRow(libraryPanel, panelType) { + continue + } + + // we have a row + if panelType == "row" { + rowPanels := panelAsJSON.Get("panels").MustArray() + err := connectLibraryPanelsRecursively(c, rowPanels, libraryPanels) + if err != nil { + return err + } continue } @@ -185,10 +242,5 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqCo } } - elementUIDs := make([]string, 0, len(libraryPanels)) - for libraryPanel := range libraryPanels { - elementUIDs = append(elementUIDs, libraryPanel) - } - - return lps.LibraryElementService.ConnectElementsToDashboard(c, elementUIDs, dash.Id) + return nil } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 467d5c15bb4..7d2e4f6e0b7 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -123,6 +123,219 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { } }) + 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.reqContext, 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, + "name": sc.initialResult.Result.Name, + }, + "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, + "name": outsidePanel.Name, + }, + "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.reqContext, []string{outsidePanel.UID, sc.initialResult.Result.UID}, dashInDB.Id) + require.NoError(t, err) + + err = sc.service.LoadLibraryPanelsForDashboard(sc.reqContext, 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{}{ @@ -310,6 +523,169 @@ func TestCleanLibraryPanelsForDashboard(t *testing.T) { } }) + 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.reqContext, 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, + "name": sc.initialResult.Result.Name, + }, + "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, + "name": outsidePanel.Name, + }, + "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, + "name": sc.initialResult.Result.Name, + }, + }, + }, + }, + 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, + "name": outsidePanel.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) + } + }) + 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{}{ @@ -438,6 +814,107 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) }) + scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all", + 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.reqContext, 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, + "name": sc.initialResult.Result.Name, + }, + "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, + "name": outsidePanel.Name, + }, + "title": "Outside row", + "type": "text", + }, + }, + } + dash := models.Dashboard{ + Title: "Testing ConnectLibraryPanelsForDashboard", + Data: simplejson.NewFromAny(dashJSON), + } + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + + err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, dashInDB) + require.NoError(t, err) + + elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, dashInDB.Id) + require.NoError(t, err) + require.Len(t, elements, 2) + require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) + require.Equal(t, outsidePanel.UID, elements[outsidePanel.UID].UID) + }) + 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{}{