From 8053f770c1ff83827748b53edafb0a8257fb4a45 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:18:27 +0100 Subject: [PATCH] Export: Fix export/import of dash with library panels (#49319) --- .betterer.results | 48 +++-- pkg/api/dashboard_test.go | 2 +- .../dashboardimport/service/service.go | 20 +- .../dashboardimport/service/service_test.go | 10 +- pkg/services/librarypanels/librarypanels.go | 39 ++-- .../librarypanels/librarypanels_test.go | 178 +++++++++--------- .../DashExportModal/DashboardExporter.test.ts | 10 +- .../DashExportModal/DashboardExporter.ts | 67 +++++-- .../components/ImportDashboardForm.tsx | 2 +- .../manage-dashboards/state/actions.ts | 6 +- .../manage-dashboards/state/reducers.test.ts | 4 +- .../manage-dashboards/state/reducers.ts | 2 +- 12 files changed, 227 insertions(+), 161 deletions(-) diff --git a/.betterer.results b/.betterer.results index da29807637f..855ad13778f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4606,7 +4606,7 @@ exports[`better eslint`] = { [90, 20, 3, "Unexpected any. Specify a different type.", "193409811"], [126, 18, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts:2274633003": [ + "public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts:3288513450": [ [22, 6, 59, "Do not use any type assertions.", "3685154675"], [22, 6, 49, "Do not use any type assertions.", "1184085652"], [25, 15, 3, "Unexpected any. Specify a different type.", "193409811"], @@ -4631,27 +4631,25 @@ exports[`better eslint`] = { [318, 17, 3, "Unexpected any. Specify a different type.", "193409811"], [325, 20, 3, "Unexpected any. Specify a different type.", "193409811"], [334, 19, 3, "Unexpected any. Specify a different type.", "193409811"], - [370, 13, 18, "Do not use any type assertions.", "3420875325"], - [371, 9, 55, "Do not use any type assertions.", "1888720338"], - [371, 17, 10, "Do not use any type assertions.", "1501063862"], - [371, 24, 3, "Unexpected any. Specify a different type.", "193409811"], - [371, 61, 3, "Unexpected any. Specify a different type.", "193409811"] + [364, 13, 18, "Do not use any type assertions.", "3420875325"], + [365, 9, 55, "Do not use any type assertions.", "1888720338"], + [365, 17, 10, "Do not use any type assertions.", "1501063862"], + [365, 24, 3, "Unexpected any. Specify a different type.", "193409811"], + [365, 61, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts:568589687": [ - [17, 9, 3, "Unexpected any. Specify a different type.", "193409811"], - [44, 9, 3, "Unexpected any. Specify a different type.", "193409811"], - [65, 43, 3, "Unexpected any. Specify a different type.", "193409811"], - [72, 45, 3, "Unexpected any. Specify a different type.", "193409811"], - [79, 30, 3, "Unexpected any. Specify a different type.", "193409811"], - [82, 25, 17, "Do not use any type assertions.", "1733699692"], - [82, 39, 3, "Unexpected any. Specify a different type.", "193409811"], - [83, 20, 33, "Do not use any type assertions.", "1729190460"], - [83, 21, 17, "Do not use any type assertions.", "1733699692"], - [83, 35, 3, "Unexpected any. Specify a different type.", "193409811"], - [148, 41, 3, "Unexpected any. Specify a different type.", "193409811"], - [176, 29, 31, "Do not use any type assertions.", "1451241646"], - [176, 29, 13, "Do not use any type assertions.", "2146830713"], - [195, 32, 3, "Unexpected any. Specify a different type.", "193409811"] + "public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts:565128344": [ + [18, 9, 3, "Unexpected any. Specify a different type.", "193409811"], + [47, 37, 3, "Unexpected any. Specify a different type.", "193409811"], + [65, 9, 3, "Unexpected any. Specify a different type.", "193409811"], + [86, 43, 3, "Unexpected any. Specify a different type.", "193409811"], + [93, 45, 3, "Unexpected any. Specify a different type.", "193409811"], + [100, 30, 3, "Unexpected any. Specify a different type.", "193409811"], + [102, 37, 17, "Do not use any type assertions.", "1733699692"], + [102, 51, 3, "Unexpected any. Specify a different type.", "193409811"], + [169, 41, 3, "Unexpected any. Specify a different type.", "193409811"], + [198, 29, 31, "Do not use any type assertions.", "1451241646"], + [198, 29, 13, "Do not use any type assertions.", "2146830713"], + [217, 32, 3, "Unexpected any. Specify a different type.", "193409811"] ], "public/app/features/dashboard/components/DashNav/DashNav.tsx:1533394562": [ [67, 87, 3, "Unexpected any. Specify a different type.", "193409811"] @@ -6193,7 +6191,7 @@ exports[`better eslint`] = { [81, 19, 3, "Unexpected any. Specify a different type.", "193409811"], [82, 25, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/manage-dashboards/components/ImportDashboardForm.tsx:72231267": [ + "public/app/features/manage-dashboards/components/ImportDashboardForm.tsx:123008784": [ [62, 28, 9, "Do not use any type assertions.", "3692209159"], [62, 34, 3, "Unexpected any. Specify a different type.", "193409811"], [125, 22, 23, "Do not use any type assertions.", "2412780303"], @@ -6221,7 +6219,7 @@ exports[`better eslint`] = { [38, 9, 3, "Unexpected any. Specify a different type.", "193409811"], [44, 23, 174, "Do not use any type assertions.", "1993531937"] ], - "public/app/features/manage-dashboards/state/actions.ts:2109642147": [ + "public/app/features/manage-dashboards/state/actions.ts:658353090": [ [43, 47, 3, "Unexpected any. Specify a different type.", "193409811"], [51, 38, 3, "Unexpected any. Specify a different type.", "193409811"], [54, 20, 3, "Unexpected any. Specify a different type.", "193409811"], @@ -6244,7 +6242,7 @@ exports[`better eslint`] = { [295, 9, 3, "Unexpected any. Specify a different type.", "193409811"], [319, 31, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/manage-dashboards/state/reducers.test.ts:4097685863": [ + "public/app/features/manage-dashboards/state/reducers.test.ts:354708423": [ [94, 25, 53, "Do not use any type assertions.", "3616064645"], [95, 23, 51, "Do not use any type assertions.", "2715348726"], [108, 25, 53, "Do not use any type assertions.", "3616064645"], @@ -6255,7 +6253,7 @@ exports[`better eslint`] = { [125, 23, 51, "Do not use any type assertions.", "2715348726"], [128, 23, 71, "Do not use any type assertions.", "1935232832"] ], - "public/app/features/manage-dashboards/state/reducers.ts:3417985415": [ + "public/app/features/manage-dashboards/state/reducers.ts:2004505684": [ [58, 13, 3, "Unexpected any. Specify a different type.", "193409811"], [68, 10, 21, "Do not use any type assertions.", "2133660528"], [76, 81, 3, "Unexpected any. Specify a different type.", "193409811"], diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 979b248fb67..d79fdbfab6f 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1158,7 +1158,7 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con return nil } -func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error { +func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error { return nil } diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 594919d9c53..dda366f5df0 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -62,6 +63,23 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb return nil, err } + // Maintain backwards compatibility by transforming array of library elements to map + libraryElements := generatedDash.Get("__elements") + libElementsArr, err := libraryElements.Array() + if err == nil { + elementMap := map[string]interface{}{} + for _, el := range libElementsArr { + libElement := simplejson.NewFromAny(el) + elementMap[libElement.Get("uid").MustString()] = el + } + libraryElements = simplejson.NewFromAny(elementMap) + } + + // No need to keep these in the stored dashboard JSON + generatedDash.Del("__elements") + generatedDash.Del("__inputs") + generatedDash.Del("__requires") + saveCmd := models.SaveDashboardCommand{ Dashboard: generatedDash, OrgId: req.User.OrgId, @@ -83,7 +101,7 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb return nil, err } - err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, savedDashboard, req.FolderId) + err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, libraryElements, generatedDash.Get("panels").MustArray(), req.FolderId) if err != nil { return nil, err } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 6281afbbf63..1211f4e5b87 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -42,7 +42,7 @@ func TestImportDashboardService(t *testing.T) { importLibraryPanelsForDashboard := false connectLibraryPanelsForDashboardCalled := false libraryPanelService := &libraryPanelServiceMock{ - importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error { + importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error { importLibraryPanelsForDashboard = true return nil }, @@ -185,8 +185,8 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa type libraryPanelServiceMock struct { librarypanels.Service - connectLibraryPanelsForDashboardFunc func(ctx context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard) error - importLibraryPanelsForDashboardFunc func(ctx context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error + connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard) error + importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error } func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard) error { @@ -197,9 +197,9 @@ func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.C return nil } -func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error { +func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error { if s.importLibraryPanelsForDashboardFunc != nil { - return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, dash, folderID) + return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID) } return nil diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 65572164016..e720778ca43 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -31,7 +31,12 @@ type Service interface { LoadLibraryPanelsForDashboard(c context.Context, dash *models.Dashboard) error CleanLibraryPanelsForDashboard(dash *models.Dashboard) error ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard) error - ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error + ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error +} + +type LibraryInfo struct { + Panels []*interface{} + LibraryPanels *simplejson.Json } // LibraryPanelService is the service for the Panel Library feature. @@ -89,7 +94,6 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle elem.Set("gridPos", gridPos) } elem.Set("id", panelAsJSON.Get("id").MustInt64()) - elem.Set("type", fmt.Sprintf("Library panel with UID: \"%s\"", UID)) elem.Set("libraryPanel", map[string]interface{}{ "uid": UID, }) @@ -253,12 +257,11 @@ func connectLibraryPanelsRecursively(c context.Context, panels []interface{}, li } // ImportLibraryPanelsForDashboard loops through all panels in dashboard JSON and creates any missing library panels in the database. -func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error { - return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, dash.Data, folderID) +func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error { + return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID) } -func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser *models.SignedInUser, parent *simplejson.Json, folderID int64) error { - panels := parent.Get("panels").MustArray() +func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser *models.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error { for _, panel := range panels { panelAsJSON := simplejson.NewFromAny(panel) libraryPanel := panelAsJSON.Get("libraryPanel") @@ -269,7 +272,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S // we have a row if panelType == "row" { - err := importLibraryPanelsRecursively(c, service, signedInUser, panelAsJSON, folderID) + err := importLibraryPanelsRecursively(c, service, signedInUser, libraryPanels, panelAsJSON.Get("panels").MustArray(), folderID) if err != nil { return err } @@ -281,22 +284,24 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S if len(UID) == 0 { return errLibraryPanelHeaderUIDMissing } - name := libraryPanel.Get("name").MustString() - if len(name) == 0 { - return errLibraryPanelHeaderNameMissing - } _, err := service.GetElement(c, signedInUser, UID) if err == nil { continue } + if errors.Is(err, libraryelements.ErrLibraryElementNotFound) { - panelAsJSON.Set("libraryPanel", - map[string]interface{}{ - "uid": UID, - "name": name, - }) - Model, err := json.Marshal(&panelAsJSON) + name := libraryPanel.Get("name").MustString() + if len(name) == 0 { + return errLibraryPanelHeaderNameMissing + } + + elementModel := libraryPanels.Get(UID).Get("model") + elementModel.Set("libraryPanel", map[string]interface{}{ + "uid": UID, + }) + + Model, err := json.Marshal(&elementModel) if err != nil { return err } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 4bee022b189..9f82feadffe 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -3,7 +3,6 @@ package librarypanels import ( "context" "encoding/json" - "fmt" "testing" "time" @@ -439,7 +438,6 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) { "libraryPanel": map[string]interface{}{ "uid": sc.initialResult.Result.UID, }, - "type": fmt.Sprintf("Library panel with UID: \"%s\"", sc.initialResult.Result.UID), }, }, } @@ -1021,30 +1019,35 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { "title": "Text - Library Panel", "type": "text", } - - dashJSON := map[string]interface{}{ - "panels": []interface{}{ - map[string]interface{}{ - "id": int64(1), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, - }, - missingModel, + var libraryElements = map[string]interface{}{ + missingUID: map[string]interface{}{ + "model": missingModel, }, } - dash := models.Dashboard{ - Title: "Testing ImportLibraryPanelsForDashboard", - Data: simplejson.NewFromAny(dashJSON), + + panels := []interface{}{ + map[string]interface{}{ + "id": int64(1), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 0, + "y": 0, + }, + }, + map[string]interface{}{ + "libraryPanel": map[string]interface{}{ + "uid": missingUID, + "name": missingName, + }, + }, } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID) + require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, 0) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID) @@ -1061,39 +1064,28 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { var existingUID = sc.initialResult.Result.UID var existingName = sc.initialResult.Result.Name - dashJSON := map[string]interface{}{ - "panels": []interface{}{ - map[string]interface{}{ - "id": int64(1), - "gridPos": map[string]interface{}{ - "h": 6, - "w": 6, - "x": 0, - "y": 0, - }, + 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(1), - "description": "Updated description", - "datasource": "Updated datasource", - "libraryPanel": map[string]interface{}{ - "uid": sc.initialResult.Result.UID, - "name": sc.initialResult.Result.Name, - }, - "title": "Updated Title", - "type": "stat", + }, + map[string]interface{}{ + "libraryPanel": map[string]interface{}{ + "uid": sc.initialResult.Result.UID, + "name": sc.initialResult.Result.Name, }, }, } - dash := models.Dashboard{ - Title: "Testing ImportLibraryPanelsForDashboard", - Data: simplejson.NewFromAny(dashJSON), - } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) require.NoError(t, err) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, sc.folder.Id) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.Id) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) @@ -1129,6 +1121,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { "title": "Outside row", "type": "text", } + var insideUID = "iK7NsyDNz" var insideName = "Inside Library Panel" var insideModel = map[string]interface{}{ @@ -1148,54 +1141,67 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { "type": "text", } - 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, - }, - }, - insideModel, - }, - }, - outsideModel, + var libraryElements = map[string]interface{}{ + outsideUID: map[string]interface{}{ + "model": outsideModel, + }, + insideUID: map[string]interface{}{ + "model": insideModel, }, } - dash := models.Dashboard{ - Title: "Testing ImportLibraryPanelsForDashboard", - Data: simplejson.NewFromAny(dashJSON), + + panels := []interface{}{ + map[string]interface{}{ + "id": int64(1), + "gridPos": map[string]interface{}{ + "h": 6, + "w": 6, + "x": 0, + "y": 0, + }, + }, + map[string]interface{}{ + "libraryPanel": map[string]interface{}{ + "uid": outsideUID, + "name": outsideName, + }, + }, + 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{}{ + "libraryPanel": map[string]interface{}{ + "uid": insideUID, + "name": insideName, + }, + }, + }, + }, } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID) require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error()) _, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID) require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB, 0) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID) diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index e844c8c5175..13c98c3416a 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -337,13 +337,10 @@ describe('given dashboard with repeated panels', () => { }); it('should add library panels as elements', () => { - const element: LibraryElementExport = exported.__elements.find( - (element: LibraryElementExport) => element.uid === 'ah8NqyDPs' - ); + const element: LibraryElementExport = exported.__elements['ah8NqyDPs']; expect(element.name).toBe('Library Panel 2'); expect(element.kind).toBe(LibraryElementKind.Panel); expect(element.model).toEqual({ - id: 17, datasource: { type: 'other2', uid: '$ds' }, targets: [{ refId: 'A', datasource: { type: 'other2', uid: '$ds' } }], type: 'graph', @@ -351,13 +348,10 @@ describe('given dashboard with repeated panels', () => { }); it('should add library panels in collapsed rows as elements', () => { - const element: LibraryElementExport = exported.__elements.find( - (element: LibraryElementExport) => element.uid === 'jL6MrxCMz' - ); + const element: LibraryElementExport = exported.__elements['jL6MrxCMz']; expect(element.name).toBe('Library Panel'); expect(element.kind).toBe(LibraryElementKind.Panel); expect(element.model).toEqual({ - id: 16, type: 'graph', datasource: { type: 'testdb', diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index dcef2dfc6b6..7abcc3ba702 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -10,6 +10,7 @@ import { LibraryElementKind } from '../../../library-panels/types'; import { isConstant, isQuery } from '../../../variables/guard'; import { VariableOption, VariableRefresh } from '../../../variables/types'; import { DashboardModel } from '../../state/DashboardModel'; +import { GridPos } from '../../state/PanelModel'; interface Input { name: string; @@ -28,6 +29,26 @@ interface Requires { }; } +interface ExternalDashboard { + __inputs: Input[]; + __elements: Record; + __requires: Array; + panels: Array; +} + +interface PanelWithExportableLibraryPanel { + gridPos: GridPos; + id: number; + libraryPanel: { + name: string; + uid: string; + }; +} + +function isExportableLibraryPanel(p: any): p is PanelWithExportableLibraryPanel { + return p.libraryPanel && typeof p.libraryPanel.name === 'string' && typeof p.libraryPanel.uid === 'string'; +} + interface DataSources { [key: string]: { name: string; @@ -79,11 +100,11 @@ export class DashboardExporter { let datasource: string = obj.datasource; let datasourceVariable: any = null; + const datasourceUid: string = (datasource as any)?.uid; // ignore data source properties that contain a variable - if (datasource && (datasource as any).uid) { - const uid = (datasource as any).uid as string; - if (uid.indexOf('$') === 0) { - datasourceVariable = variableLookup[uid.substring(1)]; + if (datasourceUid) { + if (datasourceUid.indexOf('$') === 0) { + datasourceVariable = variableLookup[datasourceUid.substring(1)]; if (datasourceVariable && datasourceVariable.current) { datasource = datasourceVariable.current.value; } @@ -150,8 +171,9 @@ export class DashboardExporter { if (isPanelModelLibraryPanel(panel)) { const { libraryPanel, ...model } = panel; const { name, uid } = libraryPanel; + const { gridPos, id, ...rest } = model; if (!libraryPanels.has(uid)) { - libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model }); + libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model: rest }); } } }; @@ -230,13 +252,36 @@ export class DashboardExporter { } } - // make inputs and requires a top thing - const newObj: { [key: string]: {} } = {}; - newObj['__inputs'] = inputs; - newObj['__elements'] = [...libraryPanels.values()]; - newObj['__requires'] = sortBy(requires, ['id']); + const __elements = [...libraryPanels.entries()].reduce>( + (prev, [curKey, curLibPanel]) => { + prev[curKey] = curLibPanel; + return prev; + }, + {} + ); + + // make inputs and requires a top thing + const newObj: ExternalDashboard = defaults( + { + __inputs: inputs, + __elements, + __requires: sortBy(requires, ['id']), + }, + saveModel + ); + + // Remove extraneous props from library panels + for (let i = 0; i < newObj.panels.length; i++) { + const libPanel = newObj.panels[i]; + if (isExportableLibraryPanel(libPanel)) { + newObj.panels[i] = { + gridPos: libPanel.gridPos, + id: libPanel.id, + libraryPanel: { uid: libPanel.libraryPanel.uid, name: libPanel.libraryPanel.name }, + }; + } + } - defaults(newObj, saveModel); return newObj; } catch (err) { console.error('Export failed:', err); diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index ea0f1cc26b6..e5f41367690 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -64,7 +64,7 @@ export const ImportDashboardForm: FC = ({ } }, [errors, getValues, isSubmitted, onSubmit]); const newLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.New) ?? []; - const existingLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.Exits) ?? []; + const existingLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.Exists) ?? []; return ( <> diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 23d0b18527e..53a892dac93 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -77,7 +77,7 @@ function processInputs(dashboardJson: any): ThunkResult { }; } -function processElements(dashboardJson?: { __elements?: LibraryElementExport[] }): ThunkResult { +function processElements(dashboardJson?: { __elements?: Record }): ThunkResult { return async function (dispatch) { if (!dashboardJson || !dashboardJson.__elements) { return; @@ -85,7 +85,7 @@ function processElements(dashboardJson?: { __elements?: LibraryElementExport[] } const libraryPanelInputs: LibraryPanelInput[] = []; - for (const element of dashboardJson.__elements) { + for (const element of Object.values(dashboardJson.__elements)) { if (element.kind !== LibraryElementKind.Panel) { continue; } @@ -110,7 +110,7 @@ function processElements(dashboardJson?: { __elements?: LibraryElementExport[] } try { const panelInDb = await getLibraryPanel(uid, true); - input.state = LibraryPanelInputState.Exits; + input.state = LibraryPanelInputState.Exists; input.model = panelInDb; } catch (e: any) { if (e.status !== 404) { diff --git a/public/app/features/manage-dashboards/state/reducers.test.ts b/public/app/features/manage-dashboards/state/reducers.test.ts index 5b25d1a394e..7b9e9d5bece 100644 --- a/public/app/features/manage-dashboards/state/reducers.test.ts +++ b/public/app/features/manage-dashboards/state/reducers.test.ts @@ -115,7 +115,7 @@ describe('importDashboardReducer', () => { setLibraryPanelInputs([ { model: { uid: 'sadjahsdk', name: 'A name', type: 'text' } as LibraryElementDTO, - state: LibraryPanelInputState.Exits, + state: LibraryPanelInputState.Exists, }, ]) ) @@ -127,7 +127,7 @@ describe('importDashboardReducer', () => { libraryPanels: [ { model: { uid: 'sadjahsdk', name: 'A name', type: 'text' } as LibraryElementDTO, - state: LibraryPanelInputState.Exits, + state: LibraryPanelInputState.Exists, }, ], }, diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index b2127692d74..1d6be8a55a8 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -27,7 +27,7 @@ export enum InputType { export enum LibraryPanelInputState { New = 'new', - Exits = 'exists', + Exists = 'exists', Different = 'different', }