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

* Chore: adds tests to reducer

* Refactor: rewrite state

* Refactor: adds library panels to export

* wip

* Refactor: adds import library panels

* Refactor: changes UI

* Chore: pushing drone

* Update public/app/features/manage-dashboards/components/ImportDashboardForm.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update public/app/features/manage-dashboards/components/ImportDashboardForm.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Chore: reverted unknown merge changes

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Hugo Häggmark
2021-09-20 10:58:24 +02:00
committed by GitHub
parent d5b885f958
commit 2696be49b9
18 changed files with 897 additions and 97 deletions

View File

@@ -1245,6 +1245,10 @@ 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 {
}
@@ -1252,6 +1256,11 @@ func (l *mockLibraryElementService) CreateElement(c *models.ReqContext, cmd libr
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

View File

@@ -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)

View File

@@ -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")
}
@@ -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)

View File

@@ -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))
@@ -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 {
@@ -228,7 +228,7 @@ func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params
return err
}
if len(libraryElements) == 0 {
return errLibraryElementNotFound
return ErrLibraryElementNotFound
}
return nil
@@ -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
}
@@ -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.
@@ -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
}
}
@@ -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{

View File

@@ -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
@@ -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)

View File

@@ -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.

View File

@@ -1,6 +1,8 @@
package librarypanels
import (
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/api/routing"
@@ -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.
@@ -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
@@ -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()
@@ -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,
},
})
@@ -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)
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
}

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
@@ -22,11 +21,11 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
const UserInDbName = "user_in_db"
const UserInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
const userInDbName = "user_in_db"
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
func TestLoadLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel",
scenarioWithLibraryPanel(t, "When an admin tries to load a dashboard with a library panel, it should copy JSON properties from library panel.",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
@@ -102,13 +101,13 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
"updated": sc.initialResult.Result.Meta.Updated,
"createdBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
@@ -276,13 +275,13 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
"updated": sc.initialResult.Result.Meta.Updated,
"createdBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.CreatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": sc.initialResult.Result.Meta.UpdatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
@@ -315,13 +314,13 @@ func TestLoadLibraryPanelsForDashboard(t *testing.T) {
"updated": outsidePanel.Meta.Updated,
"createdBy": map[string]interface{}{
"id": outsidePanel.Meta.CreatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
"updatedBy": map[string]interface{}{
"id": outsidePanel.Meta.UpdatedBy.ID,
"name": UserInDbName,
"avatarUrl": UserInDbAvatar,
"name": userInDbName,
"avatarUrl": userInDbAvatar,
},
},
},
@@ -1048,6 +1047,223 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
})
}
func TestImportLibraryPanelsForDashboard(t *testing.T) {
testScenario(t, "When an admin tries to import a dashboard with a library panel that does not exist, it should import the library panel",
func(t *testing.T, sc scenarioContext) {
var missingUID = "jL6MrxCMz"
var missingName = "Missing Library Panel"
var missingModel = map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": int64(6),
"w": int64(6),
"x": int64(0),
"y": int64(0),
},
"description": "",
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": missingUID,
"name": missingName,
},
"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,
},
}
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.reqContext, missingUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.reqContext, dashInDB, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.reqContext, missingUID)
require.NoError(t, err)
var expected = getExpected(t, element, missingUID, missingName, missingModel)
var result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithLibraryPanel(t, "When an admin tries to import a dashboard with a library panel that already exist, it should not import the library panel and existing library panel should be unchanged",
func(t *testing.T, sc scenarioContext) {
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,
},
},
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",
},
},
}
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.reqContext, existingUID)
require.NoError(t, err)
err = sc.service.ImportLibraryPanelsForDashboard(sc.reqContext, dashInDB, sc.folder.Id)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.reqContext, existingUID)
require.NoError(t, err)
var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model)
expected.FolderID = sc.initialResult.Result.FolderID
expected.Description = sc.initialResult.Result.Description
expected.Meta.FolderUID = sc.folder.Uid
expected.Meta.FolderName = sc.folder.Title
var result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
testScenario(t, "When an admin tries to import a dashboard with library panels inside and outside of rows, it should import all that do not exist",
func(t *testing.T, sc scenarioContext) {
var outsideUID = "jL6MrxCMz"
var outsideName = "Outside Library Panel"
var outsideModel = map[string]interface{}{
"id": int64(5),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 19,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": outsideUID,
"name": outsideName,
},
"title": "Outside row",
"type": "text",
}
var insideUID = "iK7NsyDNz"
var insideName = "Inside Library Panel"
var insideModel = map[string]interface{}{
"id": int64(4),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 13,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": insideUID,
"name": insideName,
},
"title": "Inside row",
"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,
},
}
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.reqContext, outsideUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
_, err = sc.elementService.GetElement(sc.reqContext, insideUID)
require.EqualError(t, err, libraryelements.ErrLibraryElementNotFound.Error())
err = sc.service.ImportLibraryPanelsForDashboard(sc.reqContext, dashInDB, 0)
require.NoError(t, err)
element, err := sc.elementService.GetElement(sc.reqContext, outsideUID)
require.NoError(t, err)
expected := getExpected(t, element, outsideUID, outsideName, outsideModel)
result := toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
element, err = sc.elementService.GetElement(sc.reqContext, insideUID)
require.NoError(t, err)
expected = getExpected(t, element, insideUID, insideName, insideModel)
result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
type libraryPanel struct {
ID int64
OrgID int64
@@ -1061,6 +1277,42 @@ type libraryPanel struct {
Meta libraryelements.LibraryElementDTOMeta
}
type libraryElementGridPos struct {
H int64 `json:"h"`
W int64 `json:"w"`
X int64 `json:"x"`
Y int64 `json:"y"`
}
type libraryElementLibraryPanel struct {
UID string `json:"uid"`
Name string `json:"name"`
}
type libraryElementModel struct {
ID int64 `json:"id"`
Datasource string `json:"datasource"`
Description string `json:"description"`
Title string `json:"title"`
Type string `json:"type"`
GridPos libraryElementGridPos `json:"gridPos"`
LibraryPanel libraryElementLibraryPanel `json:"libraryPanel"`
}
type libraryElement struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"`
UID string `json:"uid"`
Name string `json:"name"`
Kind int64 `json:"kind"`
Type string `json:"type"`
Description string `json:"description"`
Model libraryElementModel `json:"model"`
Version int64 `json:"version"`
Meta libraryelements.LibraryElementDTOMeta `json:"meta"`
}
type libraryPanelResult struct {
Result libraryPanel `json:"result"`
}
@@ -1081,6 +1333,80 @@ type folderACLItem struct {
permission models.PermissionType
}
func toLibraryElement(t *testing.T, res libraryelements.LibraryElementDTO) libraryElement {
var model = libraryElementModel{}
err := json.Unmarshal(res.Model, &model)
require.NoError(t, err)
return libraryElement{
ID: res.ID,
OrgID: res.OrgID,
FolderID: res.FolderID,
UID: res.UID,
Name: res.Name,
Type: res.Type,
Description: res.Description,
Kind: res.Kind,
Model: model,
Version: res.Version,
Meta: libraryelements.LibraryElementDTOMeta{
FolderName: res.Meta.FolderName,
FolderUID: res.Meta.FolderUID,
ConnectedDashboards: res.Meta.ConnectedDashboards,
Created: res.Meta.Created,
Updated: res.Meta.Updated,
CreatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: res.Meta.CreatedBy.ID,
Name: res.Meta.CreatedBy.Name,
AvatarURL: res.Meta.CreatedBy.AvatarURL,
},
UpdatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: res.Meta.UpdatedBy.ID,
Name: res.Meta.UpdatedBy.Name,
AvatarURL: res.Meta.UpdatedBy.AvatarURL,
},
},
}
}
func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string, name string, model map[string]interface{}) libraryElement {
marshalled, err := json.Marshal(model)
require.NoError(t, err)
var libModel libraryElementModel
err = json.Unmarshal(marshalled, &libModel)
require.NoError(t, err)
return libraryElement{
ID: res.ID,
OrgID: 1,
FolderID: 0,
UID: UID,
Name: name,
Type: "text",
Description: "",
Kind: 1,
Model: libModel,
Version: 1,
Meta: libraryelements.LibraryElementDTOMeta{
FolderName: "General",
FolderUID: "",
ConnectedDashboards: 0,
Created: res.Meta.Created,
Updated: res.Meta.Updated,
CreatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
UpdatedBy: libraryelements.LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
},
}
}
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard {
dash.FolderId = folderID
dashItem := &dashboards.SaveDashboardDTO{
@@ -1223,7 +1549,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cmd := models.CreateUserCommand{
Email: "user.in.db@test.com",
Name: "User In DB",
Login: UserInDbName,
Login: userInDbName,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
require.NoError(t, err)

View File

@@ -1,12 +1,13 @@
import { find } from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from './DashboardExporter';
import { DashboardExporter, LibraryElementExport } from './DashboardExporter';
import { DashboardModel } from '../../state/DashboardModel';
import { PanelPluginMeta } from '@grafana/data';
import { variableAdapters } from '../../../variables/adapters';
import { createConstantVariableAdapter } from '../../../variables/constant/adapter';
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter';
import { LibraryElementKind } from '../../../library-panels/types';
function getStub(arg: string) {
return Promise.resolve(stubs[arg || 'gfdb']);
@@ -84,6 +85,15 @@ describe('given dashboard with repeated panels', () => {
targets: [{ datasource: 'other' }],
},
{ id: 9, datasource: '$ds' },
{
id: 17,
datasource: '$ds',
type: 'graph',
libraryPanel: {
name: 'Library Panel 2',
uid: 'ah8NqyDPs',
},
},
{
id: 2,
repeat: 'apps',
@@ -110,6 +120,15 @@ describe('given dashboard with repeated panels', () => {
type: 'heatmap',
},
{ id: 15, repeat: null, repeatPanelId: 14 },
{
id: 16,
datasource: 'gfdb',
type: 'graph',
libraryPanel: {
name: 'Library Panel',
uid: 'jL6MrxCMz',
},
},
],
},
],
@@ -149,7 +168,7 @@ describe('given dashboard with repeated panels', () => {
});
it('should replace datasource refs in collapsed row', () => {
const panel = exported.panels[5].panels[0];
const panel = exported.panels[6].panels[0];
expect(panel.datasource).toBe('${DS_GFDB}');
});
@@ -236,6 +255,36 @@ describe('given dashboard with repeated panels', () => {
const require: any = find(exported.__requires, { name: 'OtherDB_2' });
expect(require.id).toBe('other2');
});
it('should add library panels as elements', () => {
const element: LibraryElementExport = exported.__elements.find(
(element: LibraryElementExport) => element.uid === 'ah8NqyDPs'
);
expect(element.name).toBe('Library Panel 2');
expect(element.kind).toBe(LibraryElementKind.Panel);
expect(element.model).toEqual({
id: 17,
datasource: '$ds',
type: 'graph',
fieldConfig: {
defaults: {},
overrides: [],
},
});
});
it('should add library panels in collapsed rows as elements', () => {
const element: LibraryElementExport = exported.__elements.find(
(element: LibraryElementExport) => element.uid === 'jL6MrxCMz'
);
expect(element.name).toBe('Library Panel');
expect(element.kind).toBe(LibraryElementKind.Panel);
expect(element.model).toEqual({
id: 16,
datasource: '${DS_GFDB}',
type: 'graph',
});
});
});
// Stub responses

View File

@@ -7,6 +7,8 @@ import { PanelPluginMeta } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { VariableOption, VariableRefresh } from '../../../variables/types';
import { isConstant, isQuery } from '../../../variables/guard';
import { LibraryElementKind } from '../../../library-panels/types';
import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
interface Input {
name: string;
@@ -36,6 +38,13 @@ interface DataSources {
};
}
export interface LibraryElementExport {
name: string;
uid: string;
model: any;
kind: LibraryElementKind;
}
export class DashboardExporter {
makeExportable(dashboard: DashboardModel) {
// clean up repeated rows and panels,
@@ -55,6 +64,7 @@ export class DashboardExporter {
const datasources: DataSources = {};
const promises: Array<Promise<void>> = [];
const variableLookup: { [key: string]: any } = {};
const libraryPanels: Map<string, LibraryElementExport> = new Map<string, LibraryElementExport>();
for (const variable of saveModel.getVariables()) {
variableLookup[variable.name] = variable;
@@ -132,6 +142,16 @@ export class DashboardExporter {
}
};
const processLibraryPanels = (panel: any) => {
if (isPanelModelLibraryPanel(panel)) {
const { libraryPanel, ...model } = panel;
const { name, uid } = libraryPanel;
if (!libraryPanels.has(uid)) {
libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model });
}
}
};
// check up panel data sources
for (const panel of saveModel.panels) {
processPanel(panel);
@@ -174,6 +194,17 @@ export class DashboardExporter {
inputs.push(value);
});
// we need to process all panels again after all the promises are resolved
// so all data sources, variables and targets have been templateized when we process library panels
for (const panel of saveModel.panels) {
processLibraryPanels(panel);
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
for (const rowPanel of panel.panels) {
processLibraryPanels(rowPanel);
}
}
}
// templatize constants
for (const variable of saveModel.getVariables()) {
if (isConstant(variable)) {
@@ -199,6 +230,7 @@ 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']);
defaults(newObj, saveModel);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { ReactElement, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Link, useStyles2 } from '@grafana/ui';
@@ -37,7 +37,7 @@ export const LibraryPanelCard: React.FC<LibraryPanelCardProps & { children?: JSX
title={libraryPanel.name}
description={libraryPanel.description}
plugin={panelPlugin}
onClick={() => onClick(libraryPanel)}
onClick={() => onClick?.(libraryPanel)}
onDelete={showSecondaryActions ? () => setShowDeletionModal(true) : undefined}
>
<FolderLink libraryPanel={libraryPanel} />
@@ -57,9 +57,13 @@ interface FolderLinkProps {
libraryPanel: LibraryElementDTO;
}
function FolderLink({ libraryPanel }: FolderLinkProps): JSX.Element {
function FolderLink({ libraryPanel }: FolderLinkProps): ReactElement | null {
const styles = useStyles2(getStyles);
if (!libraryPanel.meta.folderUid && !libraryPanel.meta.folderName) {
return null;
}
if (!libraryPanel.meta.folderUid) {
return (
<span className={styles.metaContainer}>

View File

@@ -7,6 +7,7 @@ import {
} from '../types';
import { DashboardSearchHit } from '../../search/types';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { lastValueFrom } from 'rxjs';
export interface GetLibraryPanelsOptions {
searchString?: string;
@@ -43,9 +44,16 @@ export async function getLibraryPanels({
return result;
}
export async function getLibraryPanel(uid: string): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().get(`/api/library-elements/${uid}`);
return result;
export async function getLibraryPanel(uid: string, isHandled = false): Promise<LibraryElementDTO> {
const response = await lastValueFrom(
getBackendSrv().fetch<{ result: LibraryElementDTO }>({
method: 'GET',
url: `/api/library-elements/${uid}`,
showSuccessAlert: !isHandled,
showErrorAlert: !isHandled,
})
);
return response.data.result;
}
export async function getLibraryPanelByName(name: string): Promise<LibraryElementDTO[]> {

View File

@@ -1,21 +1,29 @@
import React, { FC, useEffect, useState } from 'react';
import {
Button,
Field,
FormAPI,
FormFieldErrors,
FormsOnSubmit,
HorizontalGroup,
FormFieldErrors,
Input,
Field,
InputControl,
Legend,
} from '@grafana/ui';
import { DataSourcePicker } from '@grafana/runtime';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
import { validateTitle, validateUid } from '../utils/validation';
import { selectors } from '@grafana/e2e-selectors';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import {
DashboardInput,
DashboardInputs,
DataSourceInput,
ImportDashboardDTO,
LibraryPanelInputState,
} from '../state/reducers';
import { validateTitle, validateUid } from '../utils/validation';
import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanelsList';
interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> {
uidReset: boolean;
inputs: DashboardInputs;
@@ -41,6 +49,7 @@ export const ImportDashboardForm: FC<Props> = ({
}) => {
const [isSubmitted, setSubmitted] = useState(false);
const watchDataSources = watch('dataSources');
const watchFolder = watch('folder');
/*
This useEffect is needed for overwriting a dashboard. It
@@ -51,6 +60,8 @@ export const ImportDashboardForm: FC<Props> = ({
onSubmit(getValues(), {} as any);
}
}, [errors, getValues, isSubmitted, onSubmit]);
const newLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.New) ?? [];
const existingLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.Exits) ?? [];
return (
<>
@@ -136,6 +147,18 @@ export const ImportDashboardForm: FC<Props> = ({
</Field>
);
})}
<ImportDashboardLibraryPanelsList
inputs={newLibraryPanels}
label="New library panels"
description="List of new library panels that will get imported."
folderName={watchFolder.title}
/>
<ImportDashboardLibraryPanelsList
inputs={existingLibraryPanels}
label="Existing library panels"
description="List of existing library panels. These panels are not affected by the import."
folderName={watchFolder.title}
/>
<HorizontalGroup>
<Button
type="submit"

View File

@@ -0,0 +1,59 @@
import React, { ReactElement } from 'react';
import { Field, useStyles2 } from '@grafana/ui';
import { LibraryPanelInput, LibraryPanelInputState } from '../state/reducers';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { LibraryPanelCard } from '../../library-panels/components/LibraryPanelCard/LibraryPanelCard';
interface ImportDashboardLibraryPanelsListProps {
inputs: LibraryPanelInput[];
label: string;
description: string;
folderName?: string;
}
export function ImportDashboardLibraryPanelsList({
inputs,
label,
description,
folderName,
}: ImportDashboardLibraryPanelsListProps): ReactElement | null {
const styles = useStyles2(getStyles);
if (!Boolean(inputs?.length)) {
return null;
}
return (
<div className={styles.spacer}>
<Field label={label} description={description}>
<>
{inputs.map((input, index) => {
const libraryPanelIndex = `elements[${index}]`;
const libraryPanel =
input.state === LibraryPanelInputState.New
? { ...input.model, meta: { ...input.model.meta, folderName: folderName ?? 'General' } }
: { ...input.model };
return (
<div className={styles.item} key={libraryPanelIndex}>
<LibraryPanelCard libraryPanel={libraryPanel} onClick={() => undefined} />
</div>
);
})}
</>
</Field>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
spacer: css`
margin-bottom: ${theme.spacing(2)};
`,
item: css`
margin-bottom: ${theme.spacing(1)};
`,
};
}

View File

@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { dateTimeFormat } from '@grafana/data';
import { Legend, Form } from '@grafana/ui';
import { Form, Legend } from '@grafana/ui';
import { connect, ConnectedProps } from 'react-redux';
import { ImportDashboardForm } from './ImportDashboardForm';
import { clearLoadedDashboard, importDashboard } from '../state/actions';
@@ -87,7 +87,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
)}
<Form
onSubmit={this.onSubmit}
defaultValues={{ ...dashboard, constants: [], dataSources: [], folder: folder }}
defaultValues={{ ...dashboard, constants: [], dataSources: [], elements: [], folder: folder }}
validateOnMount
validateFieldsOnMount={['title', 'uid']}
validateOn="onChange"

View File

@@ -4,15 +4,21 @@ import {
clearDashboard,
ImportDashboardDTO,
InputType,
LibraryPanelInput,
LibraryPanelInputState,
setGcomDashboard,
setInputs,
setJsonDashboard,
setLibraryPanelInputs,
} from './reducers';
import { DashboardDataDTO, DashboardDTO, FolderInfo, PermissionLevelString, ThunkResult } from 'app/types';
import { appEvents } from '../../../core/core';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { getDataSourceSrv, locationService } from '@grafana/runtime';
import { DashboardSearchHit } from '../../search/types';
import { getLibraryPanel } from '../../library-panels/state/api';
import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types';
import { LibraryElementExport } from '../../dashboard/components/DashExportModal/DashboardExporter';
export function fetchGcomDashboard(id: string): ThunkResult<void> {
return async (dispatch) => {
@@ -20,6 +26,7 @@ export function fetchGcomDashboard(id: string): ThunkResult<void> {
const dashboard = await getBackendSrv().get(`/api/gnet/dashboards/${id}`);
dispatch(setGcomDashboard(dashboard));
dispatch(processInputs(dashboard.json));
dispatch(processElements(dashboard.json));
} catch (error) {
appEvents.emit(AppEvents.alertError, [error.data.message || error]);
}
@@ -30,6 +37,7 @@ export function importDashboardJson(dashboard: any): ThunkResult<void> {
return async (dispatch) => {
dispatch(setJsonDashboard(dashboard));
dispatch(processInputs(dashboard));
dispatch(processElements(dashboard));
};
}
@@ -61,6 +69,54 @@ function processInputs(dashboardJson: any): ThunkResult<void> {
};
}
function processElements(dashboardJson?: { __elements?: LibraryElementExport[] }): ThunkResult<void> {
return async function (dispatch) {
if (!dashboardJson || !dashboardJson.__elements) {
return;
}
const libraryPanelInputs: LibraryPanelInput[] = [];
for (const element of dashboardJson.__elements) {
if (element.kind !== LibraryElementKind.Panel) {
continue;
}
const model = element.model;
const { type, description } = model;
const { uid, name } = element;
const input: LibraryPanelInput = {
model: {
model,
uid,
name,
version: 0,
meta: {},
id: 0,
type,
kind: LibraryElementKind.Panel,
description,
} as LibraryElementDTO,
state: LibraryPanelInputState.New,
};
try {
const panelInDb = await getLibraryPanel(uid, true);
input.state = LibraryPanelInputState.Exits;
input.model = panelInDb;
} catch (e: any) {
if (e.status !== 404) {
throw e;
}
}
libraryPanelInputs.push(input);
}
dispatch(setLibraryPanelInputs(libraryPanelInputs));
};
}
export function clearLoadedDashboard(): ThunkResult<void> {
return (dispatch) => {
dispatch(clearDashboard());

View File

@@ -0,0 +1,134 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import {
clearDashboard,
DashboardSource,
DataSourceInput,
importDashboardReducer,
ImportDashboardState,
initialImportDashboardState,
InputType,
LibraryPanelInput,
LibraryPanelInputState,
setGcomDashboard,
setInputs,
setJsonDashboard,
setLibraryPanelInputs,
} from './reducers';
import { LibraryElementDTO } from '../../library-panels/types';
describe('importDashboardReducer', () => {
describe('when setGcomDashboard action is dispatched', () => {
it('then resulting state should be correct', () => {
reducerTester<ImportDashboardState>()
.givenReducer(importDashboardReducer, { ...initialImportDashboardState })
.whenActionIsDispatched(
setGcomDashboard({ json: { id: 1, title: 'Imported' }, updatedAt: '2001-01-01', orgName: 'Some Org' })
)
.thenStateShouldEqual({
...initialImportDashboardState,
dashboard: {
title: 'Imported',
id: null,
},
meta: { updatedAt: '2001-01-01', orgName: 'Some Org' },
source: DashboardSource.Gcom,
isLoaded: true,
});
});
});
describe('when setJsonDashboard action is dispatched', () => {
it('then resulting state should be correct', () => {
reducerTester<ImportDashboardState>()
.givenReducer(importDashboardReducer, { ...initialImportDashboardState, source: DashboardSource.Gcom })
.whenActionIsDispatched(setJsonDashboard({ id: 1, title: 'Imported' }))
.thenStateShouldEqual({
...initialImportDashboardState,
dashboard: {
title: 'Imported',
id: null,
},
source: DashboardSource.Json,
isLoaded: true,
});
});
});
describe('when clearDashboard action is dispatched', () => {
it('then resulting state should be correct', () => {
reducerTester<ImportDashboardState>()
.givenReducer(importDashboardReducer, {
...initialImportDashboardState,
dashboard: {
title: 'Imported',
id: null,
},
isLoaded: true,
})
.whenActionIsDispatched(clearDashboard())
.thenStateShouldEqual({
...initialImportDashboardState,
dashboard: {},
isLoaded: false,
});
});
});
describe('when setInputs action is dispatched', () => {
it('then resulting state should be correct', () => {
reducerTester<ImportDashboardState>()
.givenReducer(importDashboardReducer, { ...initialImportDashboardState })
.whenActionIsDispatched(
setInputs([
{ type: InputType.DataSource },
{ type: InputType.Constant },
{ type: InputType.LibraryPanel },
{ type: 'temp' },
])
)
.thenStateShouldEqual({
...initialImportDashboardState,
inputs: {
dataSources: [{ type: InputType.DataSource }] as DataSourceInput[],
constants: [{ type: InputType.Constant }] as DataSourceInput[],
libraryPanels: [],
},
});
});
});
describe('when setLibraryPanelInputs action is dispatched', () => {
it('then resulting state should be correct', () => {
reducerTester<ImportDashboardState>()
.givenReducer(importDashboardReducer, {
...initialImportDashboardState,
inputs: {
dataSources: [{ type: InputType.DataSource }] as DataSourceInput[],
constants: [{ type: InputType.Constant }] as DataSourceInput[],
libraryPanels: [{ model: { uid: 'asasAHSJ' } }] as LibraryPanelInput[],
},
})
.whenActionIsDispatched(
setLibraryPanelInputs([
{
model: { uid: 'sadjahsdk', name: 'A name', type: 'text' } as LibraryElementDTO,
state: LibraryPanelInputState.Exits,
},
])
)
.thenStateShouldEqual({
...initialImportDashboardState,
inputs: {
dataSources: [{ type: InputType.DataSource }] as DataSourceInput[],
constants: [{ type: InputType.Constant }] as DataSourceInput[],
libraryPanels: [
{
model: { uid: 'sadjahsdk', name: 'A name', type: 'text' } as LibraryElementDTO,
state: LibraryPanelInputState.Exits,
},
],
},
});
});
});
});

View File

@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit';
import { DataSourceInstanceSettings } from '@grafana/data';
import { LibraryElementDTO } from '../../library-panels/types';
export enum DashboardSource {
Gcom = 0,
@@ -12,12 +13,20 @@ export interface ImportDashboardDTO {
gnetId: string;
constants: string[];
dataSources: DataSourceInstanceSettings[];
elements: LibraryElementDTO[];
folder: { id: number; title?: string };
}
export enum InputType {
DataSource = 'datasource',
Constant = 'constant',
LibraryPanel = 'libraryPanel',
}
export enum LibraryPanelInputState {
New = 'new',
Exits = 'exists',
Different = 'different',
}
export interface DashboardInput {
@@ -32,9 +41,15 @@ export interface DataSourceInput extends DashboardInput {
pluginId: string;
}
export interface LibraryPanelInput {
model: LibraryElementDTO;
state: LibraryPanelInputState;
}
export interface DashboardInputs {
dataSources: DataSourceInput[];
constants: DashboardInput[];
libraryPanels: LibraryPanelInput[];
}
export interface ImportDashboardState {
@@ -45,7 +60,7 @@ export interface ImportDashboardState {
isLoaded: boolean;
}
const initialImportDashboardState: ImportDashboardState = {
export const initialImportDashboardState: ImportDashboardState = {
meta: { updatedAt: '', orgName: '' },
dashboard: {},
source: DashboardSource.Json,
@@ -57,47 +72,48 @@ const importDashboardSlice = createSlice({
name: 'manageDashboards',
initialState: initialImportDashboardState,
reducers: {
setGcomDashboard: (state, action: PayloadAction<any>): ImportDashboardState => {
return {
...state,
dashboard: {
...action.payload.json,
id: null,
},
meta: { updatedAt: action.payload.updatedAt, orgName: action.payload.orgName },
source: DashboardSource.Gcom,
isLoaded: true,
setGcomDashboard: (state: Draft<ImportDashboardState>, action: PayloadAction<any>) => {
state.dashboard = {
...action.payload.json,
id: null,
};
state.meta = { updatedAt: action.payload.updatedAt, orgName: action.payload.orgName };
state.source = DashboardSource.Gcom;
state.isLoaded = true;
},
setJsonDashboard: (state, action: PayloadAction<any>): ImportDashboardState => {
return {
...state,
dashboard: {
...action.payload,
id: null,
},
source: DashboardSource.Json,
isLoaded: true,
setJsonDashboard: (state: Draft<ImportDashboardState>, action: PayloadAction<any>) => {
state.dashboard = {
...action.payload,
id: null,
};
state.meta = initialImportDashboardState.meta;
state.source = DashboardSource.Json;
state.isLoaded = true;
},
clearDashboard: (state): ImportDashboardState => {
return {
...state,
dashboard: {},
isLoaded: false,
};
clearDashboard: (state: Draft<ImportDashboardState>) => {
state.dashboard = {};
state.isLoaded = false;
},
setInputs: (state, action: PayloadAction<any[]>): ImportDashboardState => ({
...state,
inputs: {
setInputs: (state: Draft<ImportDashboardState>, action: PayloadAction<any[]>) => {
state.inputs = {
dataSources: action.payload.filter((p) => p.type === InputType.DataSource),
constants: action.payload.filter((p) => p.type === InputType.Constant),
},
}),
libraryPanels: [],
};
},
setLibraryPanelInputs: (state: Draft<ImportDashboardState>, action: PayloadAction<LibraryPanelInput[]>) => {
state.inputs.libraryPanels = action.payload;
},
},
});
export const { clearDashboard, setInputs, setGcomDashboard, setJsonDashboard } = importDashboardSlice.actions;
export const {
clearDashboard,
setInputs,
setGcomDashboard,
setJsonDashboard,
setLibraryPanelInputs,
} = importDashboardSlice.actions;
export const importDashboardReducer = importDashboardSlice.reducer;