Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibraryPanels: Improves export and import of library panels between orgs #39214

Merged
merged 12 commits into from Sep 20, 2021
5 changes: 3 additions & 2 deletions docs/sources/administration/configuration.md
Expand Up @@ -471,9 +471,10 @@ Google Tag Manager ID, only enabled if you enter an ID here.
### application_insights_connection_string

If you want to track Grafana usage via Azure Application Insights, then specify _your_ Application Insights connection string. Since the connection string contains semicolons, you need to wrap it in backticks (`). By default, tracking usage is disabled.

### application_insights_endpoint_url
Optionally, use this option to override the default endpoint address for Application Insights data collecting. For details, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-endpoints?tabs=js).

Optionally, use this option to override the default endpoint address for Application Insights data collecting. For details, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-endpoints?tabs=js).
hugohaggmark marked this conversation as resolved.
Show resolved Hide resolved

<hr />

Expand Down
9 changes: 9 additions & 0 deletions pkg/api/dashboard_test.go
Expand Up @@ -1245,13 +1245,22 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.Req
return nil
}

func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard, folderID int64) error {
return nil
}

type mockLibraryElementService struct {
}

func (l *mockLibraryElementService) CreateElement(c *models.ReqContext, cmd libraryelements.CreateLibraryElementCommand) (libraryelements.LibraryElementDTO, error) {
return libraryelements.LibraryElementDTO{}, nil
}

// GetElement gets an element from a UID.
func (l *mockLibraryElementService) GetElement(c *models.ReqContext, UID string) (libraryelements.LibraryElementDTO, error) {
return libraryelements.LibraryElementDTO{}, nil
}

// GetElementsForDashboard gets all connected elements for a specific dashboard.
func (l *mockLibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]libraryelements.LibraryElementDTO, error) {
return map[string]libraryelements.LibraryElementDTO{}, nil
Expand Down
5 changes: 5 additions & 0 deletions pkg/api/plugins.go
Expand Up @@ -224,6 +224,11 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
return hs.dashboardSaveErrorToApiResponse(err)
}

err = hs.LibraryPanelService.ImportLibraryPanelsForDashboard(c, dash, apiCmd.FolderId)
if err != nil {
return response.Error(500, "Error while importing library panels", err)
}

err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dash)
if err != nil {
return response.Error(500, "Error while connecting library panels", err)
Expand Down
1 change: 1 addition & 0 deletions pkg/infra/process/root_check.go
@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows

package process
Expand Down
1 change: 1 addition & 0 deletions pkg/infra/process/root_check_windows.go
@@ -1,3 +1,4 @@
//go:build windows
// +build windows

package process
Expand Down
6 changes: 3 additions & 3 deletions pkg/services/libraryelements/api.go
Expand Up @@ -46,7 +46,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res

// getHandler handles GET /api/library-elements/:uid.
func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response {
element, err := l.getLibraryElementByUid(c)
element, err := l.getLibraryElementByUid(c, macaron.Params(c.Req)[":uid"])
if err != nil {
return toLibraryElementError(err, "Failed to get library element")
}
Expand Down Expand Up @@ -108,8 +108,8 @@ func toLibraryElementError(err error, message string) response.Response {
if errors.Is(err, errLibraryElementAlreadyExists) {
return response.Error(400, errLibraryElementAlreadyExists.Error(), err)
}
if errors.Is(err, errLibraryElementNotFound) {
return response.Error(404, errLibraryElementNotFound.Error(), err)
if errors.Is(err, ErrLibraryElementNotFound) {
return response.Error(404, ErrLibraryElementNotFound.Error(), err)
}
if errors.Is(err, errLibraryElementDashboardNotFound) {
return response.Error(404, errLibraryElementDashboardNotFound.Error(), err)
Expand Down
26 changes: 13 additions & 13 deletions pkg/services/libraryelements/database.go
Expand Up @@ -81,7 +81,7 @@ func getLibraryElement(dialect migrator.Dialect, session *sqlstore.DBSession, ui
return LibraryElementWithMeta{}, err
}
if len(elements) == 0 {
return LibraryElementWithMeta{}, errLibraryElementNotFound
return LibraryElementWithMeta{}, ErrLibraryElementNotFound
}
if len(elements) > 1 {
return LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements))
Expand Down Expand Up @@ -196,28 +196,28 @@ func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid s
if rowsAffected, err := result.RowsAffected(); err != nil {
return err
} else if rowsAffected != 1 {
return errLibraryElementNotFound
return ErrLibraryElementNotFound
}

return nil
})
}

// getLibraryElement gets a Library Element where param == value
func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params []Pair) ([]LibraryElementDTO, error) {
// getLibraryElements gets a Library Element where param == value
func getLibraryElements(c *models.ReqContext, store *sqlstore.SQLStore, params []Pair) ([]LibraryElementDTO, error) {
libraryElements := make([]LibraryElementWithMeta, 0)
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
err := store.WithDbSession(c.Req.Context(), func(session *sqlstore.DBSession) error {
builder := sqlstore.SQLBuilder{}
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", '' as folder_uid ")
builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.Dialect))
builder.Write(getFromLibraryElementDTOWithMeta(store.Dialect))
writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", 0})...)
builder.Write(" UNION ")
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", dashboard.title as folder_name ")
builder.Write(", dashboard.uid as folder_uid ")
builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.Dialect))
builder.Write(getFromLibraryElementDTOWithMeta(store.Dialect))
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
writeParamSelectorSQL(&builder, params...)
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
Expand All @@ -228,7 +228,7 @@ func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params
return err
}
if len(libraryElements) == 0 {
return errLibraryElementNotFound
return ErrLibraryElementNotFound
}

return nil
Expand Down Expand Up @@ -274,8 +274,8 @@ func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params
}

// getLibraryElementByUid gets a Library Element by uid.
func (l *LibraryElementService) getLibraryElementByUid(c *models.ReqContext) (LibraryElementDTO, error) {
libraryElements, err := l.getLibraryElements(c, []Pair{{key: "org_id", value: c.SignedInUser.OrgId}, {key: "uid", value: macaron.Params(c.Req)[":uid"]}})
func (l *LibraryElementService) getLibraryElementByUid(c *models.ReqContext, UID string) (LibraryElementDTO, error) {
libraryElements, err := getLibraryElements(c, l.SQLStore, []Pair{{key: "org_id", value: c.SignedInUser.OrgId}, {key: "uid", value: UID}})
if err != nil {
return LibraryElementDTO{}, err
}
Expand All @@ -288,7 +288,7 @@ func (l *LibraryElementService) getLibraryElementByUid(c *models.ReqContext) (Li

// getLibraryElementByName gets a Library Element by name.
func (l *LibraryElementService) getLibraryElementsByName(c *models.ReqContext) ([]LibraryElementDTO, error) {
return l.getLibraryElements(c, []Pair{{"org_id", c.SignedInUser.OrgId}, {"name", macaron.Params(c.Req)[":name"]}})
return getLibraryElements(c, l.SQLStore, []Pair{{"org_id", c.SignedInUser.OrgId}, {"name", macaron.Params(c.Req)[":name"]}})
}

// getAllLibraryElements gets all Library Elements.
Expand Down Expand Up @@ -458,7 +458,7 @@ func (l *LibraryElementService) patchLibraryElement(c *models.ReqContext, cmd pa
}

_, err := getLibraryElement(l.SQLStore.Dialect, session, updateUID, c.SignedInUser.OrgId)
if !errors.Is(err, errLibraryElementNotFound) {
if !errors.Is(err, ErrLibraryElementNotFound) {
return errLibraryElementAlreadyExists
}
}
Expand Down Expand Up @@ -498,7 +498,7 @@ func (l *LibraryElementService) patchLibraryElement(c *models.ReqContext, cmd pa
}
return err
} else if rowsAffected != 1 {
return errLibraryElementNotFound
return ErrLibraryElementNotFound
}

dto = LibraryElementDTO{
Expand Down
6 changes: 6 additions & 0 deletions pkg/services/libraryelements/libraryelements.go
Expand Up @@ -22,6 +22,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, routeRegister
// Service is a service for operating on library elements.
type Service interface {
CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error)
GetElement(c *models.ReqContext, UID string) (LibraryElementDTO, error)
GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error)
ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error
DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error
Expand All @@ -41,6 +42,11 @@ func (l *LibraryElementService) CreateElement(c *models.ReqContext, cmd CreateLi
return l.createLibraryElement(c, cmd)
}

// GetElement gets an element from a UID.
func (l *LibraryElementService) GetElement(c *models.ReqContext, UID string) (LibraryElementDTO, error) {
return l.getLibraryElementByUid(c, UID)
}

// GetElementsForDashboard gets all connected elements for a specific dashboard.
func (l *LibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) {
return l.getElementsForDashboardID(c, dashboardID)
Expand Down
4 changes: 2 additions & 2 deletions pkg/services/libraryelements/models.go
Expand Up @@ -137,8 +137,8 @@ type LibraryElementConnectionDTO struct {
var (
// errLibraryElementAlreadyExists is an error for when the user tries to add a library element that already exists.
errLibraryElementAlreadyExists = errors.New("library element with that name or UID already exists")
// errLibraryElementNotFound is an error for when a library element can't be found.
errLibraryElementNotFound = errors.New("library element could not be found")
// ErrLibraryElementNotFound is an error for when a library element can't be found.
ErrLibraryElementNotFound = errors.New("library element could not be found")
// errLibraryElementDashboardNotFound is an error for when a library element connection can't be found.
errLibraryElementDashboardNotFound = errors.New("library element connection could not be found")
// errLibraryElementHasConnections is an error for when an user deletes a library element that is connected.
Expand Down
101 changes: 87 additions & 14 deletions pkg/services/librarypanels/librarypanels.go
@@ -1,6 +1,8 @@
package librarypanels

import (
"encoding/json"
"errors"
"fmt"

"github.com/grafana/grafana/pkg/api/routing"
Expand Down Expand Up @@ -28,6 +30,7 @@ type Service interface {
LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
CleanLibraryPanelsForDashboard(dash *models.Dashboard) error
ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
ImportLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard, folderID int64) error
}

// LibraryPanelService is the service for the Panel Library feature.
Expand Down Expand Up @@ -70,20 +73,20 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle
}

// we have a library panel
uid := libraryPanel.Get("uid").MustString()
if len(uid) == 0 {
UID := libraryPanel.Get("uid").MustString()
if len(UID) == 0 {
return errLibraryPanelHeaderUIDMissing
}

elementInDB, ok := elements[uid]
elementInDB, ok := elements[UID]
if !ok {
name := libraryPanel.Get("name").MustString()
elem := parent.Get("panels").GetIndex(i)
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("type", fmt.Sprintf("Name: \"%s\", UID: \"%s\"", name, uid))
elem.Set("type", fmt.Sprintf("Name: \"%s\", UID: \"%s\"", name, UID))
elem.Set("libraryPanel", map[string]interface{}{
"uid": uid,
"uid": UID,
"name": name,
})
continue
Expand Down Expand Up @@ -166,8 +169,8 @@ func cleanLibraryPanelsRecursively(parent *simplejson.Json) error {
}

// we have a library panel
uid := libraryPanel.Get("uid").MustString()
if len(uid) == 0 {
UID := libraryPanel.Get("uid").MustString()
if len(UID) == 0 {
return errLibraryPanelHeaderUIDMissing
}
name := libraryPanel.Get("name").MustString()
Expand All @@ -177,12 +180,12 @@ func cleanLibraryPanelsRecursively(parent *simplejson.Json) error {

// 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))
ID := panelAsJSON.Get("id").MustInt64(int64(i))
parent.Get("panels").SetIndex(i, map[string]interface{}{
"id": id,
"id": ID,
"gridPos": gridPos,
"libraryPanel": map[string]interface{}{
"uid": uid,
"uid": UID,
"name": name,
},
})
Expand Down Expand Up @@ -232,15 +235,85 @@ func connectLibraryPanelsRecursively(c *models.ReqContext, panels []interface{},
}

// we have a library panel
uid := libraryPanel.Get("uid").MustString()
if len(uid) == 0 {
UID := libraryPanel.Get("uid").MustString()
if len(UID) == 0 {
return errLibraryPanelHeaderUIDMissing
}
_, exists := libraryPanels[uid]
_, exists := libraryPanels[UID]
if !exists {
libraryPanels[uid] = uid
libraryPanels[UID] = UID
}
}

return nil
}

// ImportLibraryPanelsForDashboard loops through all panels in dashboard JSON and creates any missing library panels in the database.
func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard, folderID int64) error {
return importLibraryPanelsRecursively(c, lps.LibraryElementService, dash.Data, folderID)
}

func importLibraryPanelsRecursively(c *models.ReqContext, service libraryelements.Service, parent *simplejson.Json, folderID int64) error {
panels := parent.Get("panels").MustArray()
for _, panel := range panels {
panelAsJSON := simplejson.NewFromAny(panel)
hugohaggmark marked this conversation as resolved.
Show resolved Hide resolved
libraryPanel := panelAsJSON.Get("libraryPanel")
panelType := panelAsJSON.Get("type").MustString()
if !isLibraryPanelOrRow(libraryPanel, panelType) {
continue
}

// we have a row
if panelType == "row" {
err := importLibraryPanelsRecursively(c, service, panelAsJSON, folderID)
if err != nil {
return err
}
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
}

_, err := service.GetElement(c, 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)
if err != nil {
return err
}

var cmd = libraryelements.CreateLibraryElementCommand{
FolderID: folderID,
Name: name,
Model: Model,
Kind: int64(models.PanelElement),
UID: UID,
}
_, err = service.CreateElement(c, cmd)
if err != nil {
return err
}

continue
}

return err
}

return nil
}