mirror of https://github.com/grafana/grafana.git
				
				
				
			PanelLibrary: adding library panels to Dashboard Api (#30278)
* Wip: First naive impl * Chore: fix after merge * Chore: changes after PR comments * Chore: removes unused types * Chore: adds feature toggle * Refactor: adds library panels cleanup and connect when storing dashboards * Refactor: adds feature toggle * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: adds disconnect library panels when deleting a dashboard * Chore: changes after PR comments * Tests: adds tests for LoadLibraryPanelsForDashboard * Tests: adds tests for CleanLibraryPanelsForDashboard * Tests: adds tests for ConnectLibraryPanelsForDashboard * Tests: adds tests for DisconnectLibraryPanelsForDashboard and small refactor * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/services/librarypanels/librarypanels_test.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Refactor: deletes all connections in one call and connects all in the same transaction * Chore: adds better comments * Chore: changes after PR comments Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
		
							parent
							
								
									36dc70e168
								
							
						
					
					
						commit
						b7b6632a4d
					
				|  | @ -0,0 +1,176 @@ | |||
| { | ||||
|   "annotations": { | ||||
|     "list": [ | ||||
|       { | ||||
|         "builtIn": 1, | ||||
|         "datasource": "-- Grafana --", | ||||
|         "enable": true, | ||||
|         "hide": true, | ||||
|         "iconColor": "rgba(0, 211, 255, 1)", | ||||
|         "name": "Annotations & Alerts", | ||||
|         "type": "dashboard" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "editable": true, | ||||
|   "gnetId": null, | ||||
|   "graphTooltip": 0, | ||||
|   "id": 66, | ||||
|   "links": [], | ||||
|   "panels": [ | ||||
|     { | ||||
|       "aliasColors": {}, | ||||
|       "bars": false, | ||||
|       "dashLength": 10, | ||||
|       "dashes": false, | ||||
|       "datasource": null, | ||||
|       "fieldConfig": { | ||||
|         "defaults": { | ||||
|           "custom": {} | ||||
|         }, | ||||
|         "overrides": [] | ||||
|       }, | ||||
|       "fill": 1, | ||||
|       "fillGradient": 0, | ||||
|       "gridPos": { | ||||
|         "h": 6, | ||||
|         "w": 6, | ||||
|         "x": 0, | ||||
|         "y": 0 | ||||
|       }, | ||||
|       "hiddenSeries": false, | ||||
|       "id": 5, | ||||
|       "legend": { | ||||
|         "avg": false, | ||||
|         "current": false, | ||||
|         "max": false, | ||||
|         "min": false, | ||||
|         "show": true, | ||||
|         "total": false, | ||||
|         "values": false | ||||
|       }, | ||||
|       "lines": true, | ||||
|       "linewidth": 1, | ||||
|       "nullPointMode": "null", | ||||
|       "options": { | ||||
|         "alertThreshold": true | ||||
|       }, | ||||
|       "percentage": false, | ||||
|       "pluginVersion": "7.4.0-pre", | ||||
|       "pointradius": 2, | ||||
|       "points": false, | ||||
|       "renderer": "flot", | ||||
|       "seriesOverrides": [], | ||||
|       "spaceLength": 10, | ||||
|       "stack": false, | ||||
|       "steppedLine": false, | ||||
|       "targets": [ | ||||
|         { | ||||
|           "alias": "", | ||||
|           "csvWave": { | ||||
|             "timeStep": 60, | ||||
|             "valuesCSV": "0,0,2,2,1,1" | ||||
|           }, | ||||
|           "lines": 10, | ||||
|           "points": [], | ||||
|           "pulseWave": { | ||||
|             "offCount": 3, | ||||
|             "offValue": 1, | ||||
|             "onCount": 3, | ||||
|             "onValue": 2, | ||||
|             "timeStep": 60 | ||||
|           }, | ||||
|           "refId": "A", | ||||
|           "scenarioId": "csv_metric_values", | ||||
|           "stream": { | ||||
|             "bands": 1, | ||||
|             "noise": 2.2, | ||||
|             "speed": 250, | ||||
|             "spread": 3.5, | ||||
|             "type": "signal" | ||||
|           }, | ||||
|           "stringInput": "1,20,90,30,5,0" | ||||
|         } | ||||
|       ], | ||||
|       "thresholds": [], | ||||
|       "timeFrom": null, | ||||
|       "timeRegions": [], | ||||
|       "timeShift": null, | ||||
|       "title": "Panel Title", | ||||
|       "tooltip": { | ||||
|         "shared": true, | ||||
|         "sort": 0, | ||||
|         "value_type": "individual" | ||||
|       }, | ||||
|       "type": "graph", | ||||
|       "xaxis": { | ||||
|         "buckets": null, | ||||
|         "mode": "time", | ||||
|         "name": null, | ||||
|         "show": true, | ||||
|         "values": [] | ||||
|       }, | ||||
|       "yaxes": [ | ||||
|         { | ||||
|           "format": "short", | ||||
|           "label": null, | ||||
|           "logBase": 1, | ||||
|           "max": null, | ||||
|           "min": null, | ||||
|           "show": true | ||||
|         }, | ||||
|         { | ||||
|           "format": "short", | ||||
|           "label": null, | ||||
|           "logBase": 1, | ||||
|           "max": null, | ||||
|           "min": null, | ||||
|           "show": true | ||||
|         } | ||||
|       ], | ||||
|       "yaxis": { | ||||
|         "align": false, | ||||
|         "alignLevel": null | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "gridPos": { | ||||
|         "h": 6, | ||||
|         "w": 6, | ||||
|         "x": 6, | ||||
|         "y": 0 | ||||
|       }, | ||||
|       "id": 3, | ||||
|       "libraryPanel": { | ||||
|         "uid": "MAnX2ifMk" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "gridPos": { | ||||
|         "h": 16, | ||||
|         "w": 12, | ||||
|         "x": 0, | ||||
|         "y": 6 | ||||
|       }, | ||||
|       "id": 2, | ||||
|       "libraryPanel": { | ||||
|         "uid": "g1sNpCaMz" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "schemaVersion": 27, | ||||
|   "style": "dark", | ||||
|   "tags": [], | ||||
|   "templating": { | ||||
|     "list": [] | ||||
|   }, | ||||
|   "time": { | ||||
|     "from": "now-5m", | ||||
|     "to": "now" | ||||
|   }, | ||||
|   "timepicker": {}, | ||||
|   "timezone": "", | ||||
|   "title": "Panel - Panel Library", | ||||
|   "uid": "imQX6j-Gz", | ||||
|   "version": 1 | ||||
| } | ||||
|  | @ -304,10 +304,10 @@ func (hs *HTTPServer) registerRoutes() { | |||
| 		// Dashboard
 | ||||
| 		apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { | ||||
| 			dashboardRoute.Get("/uid/:uid", routing.Wrap(hs.GetDashboard)) | ||||
| 			dashboardRoute.Delete("/uid/:uid", routing.Wrap(DeleteDashboardByUID)) | ||||
| 			dashboardRoute.Delete("/uid/:uid", routing.Wrap(hs.DeleteDashboardByUID)) | ||||
| 
 | ||||
| 			dashboardRoute.Get("/db/:slug", routing.Wrap(hs.GetDashboard)) | ||||
| 			dashboardRoute.Delete("/db/:slug", routing.Wrap(DeleteDashboardBySlug)) | ||||
| 			dashboardRoute.Delete("/db/:slug", routing.Wrap(hs.DeleteDashboardBySlug)) | ||||
| 
 | ||||
| 			dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), routing.Wrap(CalculateDashboardDiff)) | ||||
| 
 | ||||
|  |  | |||
|  | @ -147,6 +147,14 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { | |||
| 	// make sure db version is in sync with json model version
 | ||||
| 	dash.Data.Set("version", dash.Version) | ||||
| 
 | ||||
| 	if hs.Cfg.IsPanelLibraryEnabled() { | ||||
| 		// load library panels JSON for this dashboard
 | ||||
| 		err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(dash) | ||||
| 		if err != nil { | ||||
| 			return response.Error(500, "Error while loading library panels", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	dto := dtos.DashboardFullWithMeta{ | ||||
| 		Dashboard: dash.Data, | ||||
| 		Meta:      meta, | ||||
|  | @ -181,7 +189,7 @@ func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*models | |||
| 	return query.Result, nil | ||||
| } | ||||
| 
 | ||||
| func DeleteDashboardBySlug(c *models.ReqContext) response.Response { | ||||
| func (hs *HTTPServer) DeleteDashboardBySlug(c *models.ReqContext) response.Response { | ||||
| 	query := models.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(&query); err != nil { | ||||
|  | @ -192,14 +200,14 @@ func DeleteDashboardBySlug(c *models.ReqContext) response.Response { | |||
| 		return response.JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": models.ErrDashboardsWithSameSlugExists.Error()}) | ||||
| 	} | ||||
| 
 | ||||
| 	return deleteDashboard(c) | ||||
| 	return hs.deleteDashboard(c) | ||||
| } | ||||
| 
 | ||||
| func DeleteDashboardByUID(c *models.ReqContext) response.Response { | ||||
| 	return deleteDashboard(c) | ||||
| func (hs *HTTPServer) DeleteDashboardByUID(c *models.ReqContext) response.Response { | ||||
| 	return hs.deleteDashboard(c) | ||||
| } | ||||
| 
 | ||||
| func deleteDashboard(c *models.ReqContext) response.Response { | ||||
| func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { | ||||
| 	dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) | ||||
| 	if rsp != nil { | ||||
| 		return rsp | ||||
|  | @ -210,6 +218,14 @@ func deleteDashboard(c *models.ReqContext) response.Response { | |||
| 		return dashboardGuardianResponse(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if hs.Cfg.IsPanelLibraryEnabled() { | ||||
| 		// disconnect all library panels for this dashboard
 | ||||
| 		err := hs.LibraryPanelService.DisconnectLibraryPanelsForDashboard(dash) | ||||
| 		if err != nil { | ||||
| 			hs.log.Error("Failed to disconnect library panels", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err := dashboards.NewService().DeleteDashboard(dash.Id, c.OrgId) | ||||
| 	if err != nil { | ||||
| 		var dashboardErr models.DashboardErr | ||||
|  | @ -256,6 +272,14 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa | |||
| 		allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name) | ||||
| 	} | ||||
| 
 | ||||
| 	if hs.Cfg.IsPanelLibraryEnabled() { | ||||
| 		// clean up all unnecessary library panels JSON properties so we store a minimum JSON
 | ||||
| 		err = hs.LibraryPanelService.CleanLibraryPanelsForDashboard(dash) | ||||
| 		if err != nil { | ||||
| 			return response.Error(500, "Error while cleaning library panels", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	dashItem := &dashboards.SaveDashboardDTO{ | ||||
| 		Dashboard: dash, | ||||
| 		Message:   cmd.Message, | ||||
|  | @ -288,6 +312,14 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if hs.Cfg.IsPanelLibraryEnabled() { | ||||
| 		// connect library panels for this dashboard after the dashboard is stored and has an ID
 | ||||
| 		err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dashboard) | ||||
| 		if err != nil { | ||||
| 			return response.Error(500, "Error while connecting library panels", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	c.TimeRequest(metrics.MApiDashboardSave) | ||||
| 	return response.JSON(200, util.DynMap{ | ||||
| 		"status":  "success", | ||||
|  |  | |||
|  | @ -165,7 +165,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardBySlug(sc) | ||||
| 					CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 
 | ||||
| 					assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
|  | @ -175,7 +175,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardByUID(sc) | ||||
| 					CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 
 | ||||
| 					assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
|  | @ -230,7 +230,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardBySlug(sc) | ||||
| 					CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 					assert.Equal(t, 200, sc.resp.Code) | ||||
| 					assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 				}) | ||||
|  | @ -239,7 +239,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardByUID(sc) | ||||
| 					CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 					assert.Equal(t, 200, sc.resp.Code) | ||||
| 					assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 				}) | ||||
|  | @ -354,7 +354,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardBySlug(sc) | ||||
| 					CallDeleteDashboardBySlug(sc, hs) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 					assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 				}) | ||||
|  | @ -363,7 +363,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardByUID(sc) | ||||
| 					CallDeleteDashboardByUID(sc, hs) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 					assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 				}) | ||||
|  | @ -414,7 +414,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardBySlug(sc) | ||||
| 					CallDeleteDashboardBySlug(sc, hs) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 					assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 				}) | ||||
|  | @ -423,7 +423,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 				"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 					state := setUp() | ||||
| 
 | ||||
| 					CallDeleteDashboardByUID(sc) | ||||
| 					CallDeleteDashboardByUID(sc, hs) | ||||
| 					assert.Equal(t, 403, sc.resp.Code) | ||||
| 					assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 				}) | ||||
|  | @ -492,7 +492,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardBySlug(sc) | ||||
| 				CallDeleteDashboardBySlug(sc, hs) | ||||
| 				assert.Equal(t, 200, sc.resp.Code) | ||||
| 				assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 			}) | ||||
|  | @ -500,7 +500,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardByUID(sc) | ||||
| 				CallDeleteDashboardByUID(sc, hs) | ||||
| 				assert.Equal(t, 200, sc.resp.Code) | ||||
| 				assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 			}) | ||||
|  | @ -570,7 +570,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardBySlug(sc) | ||||
| 				CallDeleteDashboardBySlug(sc, hs) | ||||
| 				assert.Equal(t, 403, sc.resp.Code) | ||||
| 				assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 			}) | ||||
|  | @ -578,7 +578,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardByUID(sc) | ||||
| 				CallDeleteDashboardByUID(sc, hs) | ||||
| 				assert.Equal(t, 403, sc.resp.Code) | ||||
| 				assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 			}) | ||||
|  | @ -624,7 +624,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardBySlug(sc) | ||||
| 				CallDeleteDashboardBySlug(sc, hs) | ||||
| 				assert.Equal(t, 200, sc.resp.Code) | ||||
| 				assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 			}) | ||||
|  | @ -632,7 +632,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardByUID(sc) | ||||
| 				CallDeleteDashboardByUID(sc, hs) | ||||
| 				assert.Equal(t, 200, sc.resp.Code) | ||||
| 				assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 			}) | ||||
|  | @ -690,7 +690,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardBySlug(sc) | ||||
| 				CallDeleteDashboardBySlug(sc, hs) | ||||
| 				assert.Equal(t, 403, sc.resp.Code) | ||||
| 				assert.Equal(t, "child-dash", state.dashQueries[0].Slug) | ||||
| 			}) | ||||
|  | @ -698,7 +698,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 			loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { | ||||
| 				state := setUpInner() | ||||
| 
 | ||||
| 				CallDeleteDashboardByUID(sc) | ||||
| 				CallDeleteDashboardByUID(sc, hs) | ||||
| 				assert.Equal(t, 403, sc.resp.Code) | ||||
| 				assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) | ||||
| 			}) | ||||
|  | @ -744,7 +744,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 		role := models.ROLE_EDITOR | ||||
| 
 | ||||
| 		loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { | ||||
| 			CallDeleteDashboardBySlug(sc) | ||||
| 			CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 
 | ||||
| 			assert.Equal(t, 412, sc.resp.Code) | ||||
| 			result := sc.ToJSON() | ||||
|  | @ -1033,7 +1033,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 		loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { | ||||
| 			setUp() | ||||
| 
 | ||||
| 			CallDeleteDashboardBySlug(sc) | ||||
| 			CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 
 | ||||
| 			assert.Equal(t, 400, sc.resp.Code) | ||||
| 			result := sc.ToJSON() | ||||
|  | @ -1043,7 +1043,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { | |||
| 		loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { | ||||
| 			setUp() | ||||
| 
 | ||||
| 			CallDeleteDashboardByUID(sc) | ||||
| 			CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) | ||||
| 
 | ||||
| 			assert.Equal(t, 400, sc.resp.Code) | ||||
| 			result := sc.ToJSON() | ||||
|  | @ -1141,21 +1141,21 @@ func callGetDashboardVersions(sc *scenarioContext) { | |||
| 	sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() | ||||
| } | ||||
| 
 | ||||
| func CallDeleteDashboardBySlug(sc *scenarioContext) { | ||||
| func CallDeleteDashboardBySlug(sc *scenarioContext, hs *HTTPServer) { | ||||
| 	bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	sc.handlerFunc = DeleteDashboardBySlug | ||||
| 	sc.handlerFunc = hs.DeleteDashboardBySlug | ||||
| 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() | ||||
| } | ||||
| 
 | ||||
| func CallDeleteDashboardByUID(sc *scenarioContext) { | ||||
| func CallDeleteDashboardByUID(sc *scenarioContext, hs *HTTPServer) { | ||||
| 	bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	sc.handlerFunc = DeleteDashboardByUID | ||||
| 	sc.handlerFunc = hs.DeleteDashboardByUID | ||||
| 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/services/contexthandler" | ||||
| 	"github.com/grafana/grafana/pkg/services/datasources" | ||||
| 	"github.com/grafana/grafana/pkg/services/hooks" | ||||
| 	"github.com/grafana/grafana/pkg/services/librarypanels" | ||||
| 	"github.com/grafana/grafana/pkg/services/login" | ||||
| 	"github.com/grafana/grafana/pkg/services/provisioning" | ||||
| 	"github.com/grafana/grafana/pkg/services/quota" | ||||
|  | @ -79,6 +80,7 @@ type HTTPServer struct { | |||
| 	Live                 *live.GrafanaLive                  `inject:""` | ||||
| 	ContextHandler       *contexthandler.ContextHandler     `inject:""` | ||||
| 	SQLStore             *sqlstore.SQLStore                 `inject:""` | ||||
| 	LibraryPanelService  *librarypanels.LibraryPanelService `inject:""` | ||||
| 	Listener             net.Listener | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -181,6 +181,17 @@ func (j *Json) GetIndex(index int) *Json { | |||
| 	return &Json{nil} | ||||
| } | ||||
| 
 | ||||
| // SetIndex modifies `Json` array by `index` and `value`
 | ||||
| // for `index` in its `array` representation
 | ||||
| func (j *Json) SetIndex(index int, val interface{}) { | ||||
| 	a, err := j.Array() | ||||
| 	if err == nil { | ||||
| 		if len(a) > index { | ||||
| 			a[index] = val | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // CheckGet returns a pointer to a new `Json` object and
 | ||||
| // a `bool` identifying success or failure
 | ||||
| //
 | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ import ( | |||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
|  | @ -40,10 +42,8 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre | |||
| 	return libraryPanel, err | ||||
| } | ||||
| 
 | ||||
| // connectDashboard adds a connection between a Library Panel and a Dashboard.
 | ||||
| func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { | ||||
| 	err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { | ||||
| 		panel, err := getLibraryPanel(session, uid, c.SignedInUser.OrgId) | ||||
| func connectDashboard(session *sqlstore.DBSession, dialect migrator.Dialect, user *models.SignedInUser, uid string, dashboardID int64) error { | ||||
| 	panel, err := getLibraryPanel(session, uid, user.OrgId) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -54,15 +54,36 @@ func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid strin | |||
| 		DashboardID:    dashboardID, | ||||
| 		LibraryPanelID: panel.ID, | ||||
| 		Created:        time.Now(), | ||||
| 			CreatedBy:      c.SignedInUser.UserId, | ||||
| 		CreatedBy:      user.UserId, | ||||
| 	} | ||||
| 	if _, err := session.Insert(&libraryPanelDashboard); err != nil { | ||||
| 			if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { | ||||
| 		if dialect.IsUniqueConstraintViolation(err) { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // connectDashboard adds a connection between a Library Panel and a Dashboard.
 | ||||
| func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { | ||||
| 	err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { | ||||
| 		return connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID) | ||||
| 	}) | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard.
 | ||||
| func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error { | ||||
| 	err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { | ||||
| 		for _, uid := range uids { | ||||
| 			err := connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return err | ||||
|  | @ -110,6 +131,23 @@ func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid st | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // disconnectLibraryPanelsForDashboard deletes connections for all Library Panels in a Dashboard.
 | ||||
| func (lps *LibraryPanelService) disconnectLibraryPanelsForDashboard(dashboardID int64, panelCount int64) error { | ||||
| 	return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { | ||||
| 		result, err := session.Exec("DELETE FROM library_panel_dashboard WHERE dashboard_id=?", dashboardID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if rowsAffected, err := result.RowsAffected(); err != nil { | ||||
| 			return err | ||||
| 		} else if rowsAffected != panelCount { | ||||
| 			lps.log.Warn("Number of disconnects does not match number of panels", "dashboard", dashboardID, "rowsAffected", rowsAffected, "panelCount", panelCount) | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func getLibraryPanel(session *sqlstore.DBSession, uid string, orgID int64) (LibraryPanel, error) { | ||||
| 	libraryPanels := make([]LibraryPanel, 0) | ||||
| 	session.Table("library_panel") | ||||
|  | @ -183,6 +221,33 @@ func (lps *LibraryPanelService) getConnectedDashboards(c *models.ReqContext, uid | |||
| 	return connectedDashboardIDs, err | ||||
| } | ||||
| 
 | ||||
| func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(dashboardID int64) (map[string]LibraryPanel, error) { | ||||
| 	libraryPanelMap := make(map[string]LibraryPanel) | ||||
| 	err := lps.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error { | ||||
| 		sql := `SELECT | ||||
| 				lp.id, lp.org_id, lp.folder_id, lp.uid, lp.name, lp.model, lp.created, lp.created_by, lp.updated, updated_by | ||||
| 			FROM | ||||
| 				library_panel_dashboard AS lpd | ||||
| 			INNER JOIN | ||||
| 				library_panel AS lp ON lpd.librarypanel_id = lp.id AND lpd.dashboard_id=?` | ||||
| 
 | ||||
| 		var libraryPanels []LibraryPanel | ||||
| 		sess := session.SQL(sql, dashboardID) | ||||
| 		err := sess.Find(&libraryPanels) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, panel := range libraryPanels { | ||||
| 			libraryPanelMap[panel.UID] = panel | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return libraryPanelMap, err | ||||
| } | ||||
| 
 | ||||
| // patchLibraryPanel updates a Library Panel.
 | ||||
| func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patchLibraryPanelCommand, uid string) (LibraryPanel, error) { | ||||
| 	var libraryPanel LibraryPanel | ||||
|  |  | |||
|  | @ -1,8 +1,12 @@ | |||
| package librarypanels | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/api/routing" | ||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/registry" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator" | ||||
|  | @ -39,6 +43,157 @@ func (lps *LibraryPanelService) IsEnabled() bool { | |||
| 	return lps.Cfg.IsPanelLibraryEnabled() | ||||
| } | ||||
| 
 | ||||
| // LoadLibraryPanelsForDashboard loops through all panels in dashboard JSON and replaces any library panel JSON
 | ||||
| // with JSON stored for library panel in db.
 | ||||
| func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(dash *models.Dashboard) error { | ||||
| 	if !lps.IsEnabled() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	libraryPanels, err := lps.getLibraryPanelsForDashboardID(dash.Id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	panels := dash.Data.Get("panels").MustArray() | ||||
| 	for i, panel := range panels { | ||||
| 		panelAsJSON := simplejson.NewFromAny(panel) | ||||
| 		libraryPanel := panelAsJSON.Get("libraryPanel") | ||||
| 		if libraryPanel.Interface() == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// we have a library panel
 | ||||
| 		uid := libraryPanel.Get("uid").MustString() | ||||
| 		if len(uid) == 0 { | ||||
| 			return errLibraryPanelHeaderUIDMissing | ||||
| 		} | ||||
| 
 | ||||
| 		libraryPanelInDB, ok := libraryPanels[uid] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("found connection to library panel %q that isn't in database", uid) | ||||
| 		} | ||||
| 
 | ||||
| 		// we have a match between what is stored in db and in dashboard json
 | ||||
| 		libraryPanelModel, err := libraryPanelInDB.Model.MarshalJSON() | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("could not marshal library panel JSON: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		libraryPanelModelAsJSON, err := simplejson.NewJson(libraryPanelModel) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("could not convert library panel to simplejson model: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		// set the library panel json as the new panel json in dashboard json
 | ||||
| 		dash.Data.Get("panels").SetIndex(i, libraryPanelModelAsJSON.Interface()) | ||||
| 
 | ||||
| 		// set dashboard specific props
 | ||||
| 		elem := dash.Data.Get("panels").GetIndex(i) | ||||
| 		elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap()) | ||||
| 		elem.Set("id", panelAsJSON.Get("id").MustInt64()) | ||||
| 		elem.Set("libraryPanel", map[string]interface{}{ | ||||
| 			"uid":  libraryPanelInDB.UID, | ||||
| 			"name": libraryPanelInDB.Name, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CleanLibraryPanelsForDashboard loops through all panels in dashboard JSON and cleans up any library panel JSON so that
 | ||||
| // only the necessary JSON properties remain when storing the dashboard JSON.
 | ||||
| func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error { | ||||
| 	if !lps.IsEnabled() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	panels := dash.Data.Get("panels").MustArray() | ||||
| 	for i, panel := range panels { | ||||
| 		panelAsJSON := simplejson.NewFromAny(panel) | ||||
| 		libraryPanel := panelAsJSON.Get("libraryPanel") | ||||
| 		if libraryPanel.Interface() == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// we have a library panel
 | ||||
| 		uid := libraryPanel.Get("uid").MustString() | ||||
| 		if len(uid) == 0 { | ||||
| 			return errLibraryPanelHeaderUIDMissing | ||||
| 		} | ||||
| 		name := libraryPanel.Get("name").MustString() | ||||
| 		if len(name) == 0 { | ||||
| 			return errLibraryPanelHeaderNameMissing | ||||
| 		} | ||||
| 
 | ||||
| 		// keep only the necessary JSON properties, the rest of the properties should be safely stored in library_panels table
 | ||||
| 		gridPos := panelAsJSON.Get("gridPos").MustMap() | ||||
| 		id := panelAsJSON.Get("id").MustInt64(int64(i)) | ||||
| 		dash.Data.Get("panels").SetIndex(i, map[string]interface{}{ | ||||
| 			"id":      id, | ||||
| 			"gridPos": gridPos, | ||||
| 			"libraryPanel": map[string]interface{}{ | ||||
| 				"uid":  uid, | ||||
| 				"name": name, | ||||
| 			}, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
 | ||||
| func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error { | ||||
| 	if !lps.IsEnabled() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	panels := dash.Data.Get("panels").MustArray() | ||||
| 	var libraryPanels []string | ||||
| 	for _, panel := range panels { | ||||
| 		panelAsJSON := simplejson.NewFromAny(panel) | ||||
| 		libraryPanel := panelAsJSON.Get("libraryPanel") | ||||
| 		if libraryPanel.Interface() == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// we have a library panel
 | ||||
| 		uid := libraryPanel.Get("uid").MustString() | ||||
| 		if len(uid) == 0 { | ||||
| 			return errLibraryPanelHeaderUIDMissing | ||||
| 		} | ||||
| 		libraryPanels = append(libraryPanels, uid) | ||||
| 	} | ||||
| 
 | ||||
| 	return lps.connectLibraryPanelsForDashboard(c, libraryPanels, dash.Id) | ||||
| } | ||||
| 
 | ||||
| // DisconnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and disconnects any library panels from the dashboard.
 | ||||
| func (lps *LibraryPanelService) DisconnectLibraryPanelsForDashboard(dash *models.Dashboard) error { | ||||
| 	if !lps.IsEnabled() { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	panels := dash.Data.Get("panels").MustArray() | ||||
| 	panelCount := int64(0) | ||||
| 	for _, panel := range panels { | ||||
| 		panelAsJSON := simplejson.NewFromAny(panel) | ||||
| 		libraryPanel := panelAsJSON.Get("libraryPanel") | ||||
| 		if libraryPanel.Interface() == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// we have a library panel
 | ||||
| 		uid := libraryPanel.Get("uid").MustString() | ||||
| 		if len(uid) == 0 { | ||||
| 			return errLibraryPanelHeaderUIDMissing | ||||
| 		} | ||||
| 		panelCount++ | ||||
| 	} | ||||
| 
 | ||||
| 	return lps.disconnectLibraryPanelsForDashboard(dash.Id, panelCount) | ||||
| } | ||||
| 
 | ||||
| // AddMigration defines database migrations.
 | ||||
| // If Panel Library is not enabled does nothing.
 | ||||
| func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { | ||||
|  |  | |||
|  | @ -2,9 +2,12 @@ package librarypanels | |||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"gopkg.in/macaron.v1" | ||||
|  | @ -30,7 +33,7 @@ func TestCreateLibraryPanel(t *testing.T) { | |||
| func TestConnectLibraryPanel(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to create a connection for a library panel that does not exist, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"}) | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"}) | ||||
| 			response := sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 404, response.Status()) | ||||
| 		}) | ||||
|  | @ -45,7 +48,7 @@ func TestConnectLibraryPanel(t *testing.T) { | |||
| 			err := json.Unmarshal(response.Body(), &result) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
|  | @ -97,7 +100,7 @@ func TestDeleteLibraryPanel(t *testing.T) { | |||
| func TestDisconnectLibraryPanel(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to remove a connection with a library panel that does not exist, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", "dashboardId": "1"}) | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown", ":dashboardId": "1"}) | ||||
| 			response := sc.service.disconnectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 404, response.Status()) | ||||
| 		}) | ||||
|  | @ -112,7 +115,7 @@ func TestDisconnectLibraryPanel(t *testing.T) { | |||
| 			err := json.Unmarshal(response.Body(), &result) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.disconnectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 404, response.Status()) | ||||
| 		}) | ||||
|  | @ -127,7 +130,7 @@ func TestDisconnectLibraryPanel(t *testing.T) { | |||
| 			err := json.Unmarshal(response.Body(), &result) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, "dashboardId": "1"}) | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 			response = sc.service.disconnectHandler(sc.reqContext) | ||||
|  | @ -330,7 +333,7 @@ func TestPatchLibraryPanel(t *testing.T) { | |||
| 								{ | ||||
| 								  "datasource": "${DS_GDEV-TESTDATA}", | ||||
| 								  "id": 1, | ||||
| 								  "name": "Model - New name", | ||||
| 								  "title": "Model - New name", | ||||
| 								  "type": "text" | ||||
| 								} | ||||
| 							`), | ||||
|  | @ -344,7 +347,7 @@ func TestPatchLibraryPanel(t *testing.T) { | |||
| 			require.NoError(t, err) | ||||
| 			existing.Result.FolderID = int64(2) | ||||
| 			existing.Result.Name = "Panel - New name" | ||||
| 			existing.Result.Model["name"] = "Model - New name" | ||||
| 			existing.Result.Model["title"] = "Model - New name" | ||||
| 			if diff := cmp.Diff(existing.Result, result.Result, getCompareOptions()...); diff != "" { | ||||
| 				t.Fatalf("Result mismatch (-want +got):\n%s", diff) | ||||
| 			} | ||||
|  | @ -516,6 +519,578 @@ func TestPatchLibraryPanel(t *testing.T) { | |||
| 		}) | ||||
| } | ||||
| 
 | ||||
| func TestLoadLibraryPanelsForDashboard(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.LoadLibraryPanelsForDashboard(&dash) | ||||
| 			require.NoError(t, err) | ||||
| 			expectedJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			expected := simplejson.NewFromAny(expectedJSON) | ||||
| 			if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" { | ||||
| 				t.Fatalf("Result mismatch (-want +got):\n%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to load a dashboard with a library panel without uid, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.LoadLibraryPanelsForDashboard(&dash) | ||||
| 			require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to load a dashboard with a library panel that is not connected, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.LoadLibraryPanelsForDashboard(&dash) | ||||
| 			require.EqualError(t, err, fmt.Errorf("found connection to library panel %q that isn't in database", existing.Result.UID).Error()) | ||||
| 		}) | ||||
| } | ||||
| 
 | ||||
| func TestCleanLibraryPanelsForDashboard(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to store a dashboard with a library panel, it should just keep the correct JSON properties in library panel", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.CleanLibraryPanelsForDashboard(&dash) | ||||
| 			require.NoError(t, err) | ||||
| 			expectedJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			expected := simplejson.NewFromAny(expectedJSON) | ||||
| 			if diff := cmp.Diff(expected.Interface(), dash.Data.Interface(), getCompareOptions()...); diff != "" { | ||||
| 				t.Fatalf("Result mismatch (-want +got):\n%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.CleanLibraryPanelsForDashboard(&dash) | ||||
| 			require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to store a dashboard with a library panel without name, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid": existing.Result.UID, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   1, | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.CleanLibraryPanelsForDashboard(&dash) | ||||
| 			require.EqualError(t, err, errLibraryPanelHeaderNameMissing.Error()) | ||||
| 		}) | ||||
| } | ||||
| 
 | ||||
| func TestConnectLibraryPanelsForDashboard(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to store a dashboard with a library panel, it should connect the two", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   int64(1), | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) | ||||
| 			response = sc.service.getConnectedDashboardsHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var dashResult libraryPanelDashboardsResult | ||||
| 			err = json.Unmarshal(response.Body(), &dashResult) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Len(t, dashResult.Result, 1) | ||||
| 			require.Equal(t, int64(1), dashResult.Result[0]) | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to store a dashboard with a library panel without uid, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   int64(1), | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.ConnectLibraryPanelsForDashboard(sc.reqContext, &dash) | ||||
| 			require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) | ||||
| 		}) | ||||
| } | ||||
| 
 | ||||
| func TestDisconnectLibraryPanelsForDashboard(t *testing.T) { | ||||
| 	testScenario(t, "When an admin tries to delete a dashboard with a library panel, it should disconnect the two", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"uid":  existing.Result.UID, | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   int64(1), | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.DisconnectLibraryPanelsForDashboard(&dash) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID}) | ||||
| 			response = sc.service.getConnectedDashboardsHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var dashResult libraryPanelDashboardsResult | ||||
| 			err = json.Unmarshal(response.Body(), &dashResult) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Empty(t, dashResult.Result) | ||||
| 		}) | ||||
| 
 | ||||
| 	testScenario(t, "When an admin tries to delete a dashboard with a library panel without uid, it should fail", | ||||
| 		func(t *testing.T, sc scenarioContext) { | ||||
| 			command := getCreateCommand(1, "Text - Library Panel1") | ||||
| 			response := sc.service.createHandler(sc.reqContext, command) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			var existing libraryPanelResult | ||||
| 			err := json.Unmarshal(response.Body(), &existing) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			sc.reqContext.ReplaceAllParams(map[string]string{":uid": existing.Result.UID, ":dashboardId": "1"}) | ||||
| 			response = sc.service.connectHandler(sc.reqContext) | ||||
| 			require.Equal(t, 200, response.Status()) | ||||
| 
 | ||||
| 			dashJSON := map[string]interface{}{ | ||||
| 				"panels": []interface{}{ | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(1), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 0, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 					}, | ||||
| 					map[string]interface{}{ | ||||
| 						"id": int64(2), | ||||
| 						"gridPos": map[string]interface{}{ | ||||
| 							"h": 6, | ||||
| 							"w": 6, | ||||
| 							"x": 6, | ||||
| 							"y": 0, | ||||
| 						}, | ||||
| 						"datasource": "${DS_GDEV-TESTDATA}", | ||||
| 						"libraryPanel": map[string]interface{}{ | ||||
| 							"name": existing.Result.Name, | ||||
| 						}, | ||||
| 						"title": "Text - Library Panel", | ||||
| 						"type":  "text", | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
| 			dash := models.Dashboard{ | ||||
| 				Id:   int64(1), | ||||
| 				Data: simplejson.NewFromAny(dashJSON), | ||||
| 			} | ||||
| 
 | ||||
| 			err = sc.service.DisconnectLibraryPanelsForDashboard(&dash) | ||||
| 			require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) | ||||
| 		}) | ||||
| } | ||||
| 
 | ||||
| type libraryPanel struct { | ||||
| 	ID        int64                  `json:"id"` | ||||
| 	OrgID     int64                  `json:"orgId"` | ||||
|  | @ -570,7 +1145,7 @@ func getCreateCommand(folderID int64, name string) createLibraryPanelCommand { | |||
| 			{ | ||||
| 			  "datasource": "${DS_GDEV-TESTDATA}", | ||||
| 			  "id": 1, | ||||
| 			  "name": "Text - Library Panel", | ||||
| 			  "title": "Text - Library Panel", | ||||
| 			  "type": "text" | ||||
| 			} | ||||
| 		`), | ||||
|  |  | |||
|  | @ -40,6 +40,10 @@ var ( | |||
| 	errLibraryPanelNotFound = errors.New("library panel could not be found") | ||||
| 	// errLibraryPanelDashboardNotFound is an error for when a library panel connection can't be found.
 | ||||
| 	errLibraryPanelDashboardNotFound = errors.New("library panel connection could not be found") | ||||
| 	// errLibraryPanelHeaderUIDMissing is an error for when a library panel header is missing the uid property.
 | ||||
| 	errLibraryPanelHeaderUIDMissing = errors.New("library panel header is missing required property uid") | ||||
| 	// errLibraryPanelHeaderNameMissing is an error for when a library panel header is missing the name property.
 | ||||
| 	errLibraryPanelHeaderNameMissing = errors.New("library panel header is missing required property name") | ||||
| ) | ||||
| 
 | ||||
| // Commands
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue