mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Adds panel type filter and sorting (#33425)
* Wip: inital commit * Chore: updating api * Refactor: adds description search and sorting * Refactor: adds panel filtering * Refactor: limits the height of select * Tests: updates snapshot * Refactor: small UI improvements
This commit is contained in:
parent
eaf7decf5a
commit
20ee0e9601
@ -81,7 +81,15 @@ func (lps *LibraryPanelService) getHandler(c *models.ReqContext) response.Respon
|
|||||||
|
|
||||||
// getAllHandler handles GET /api/library-panels/.
|
// getAllHandler handles GET /api/library-panels/.
|
||||||
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response {
|
func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Response {
|
||||||
libraryPanels, err := lps.getAllLibraryPanels(c, c.QueryInt("perPage"), c.QueryInt("page"), c.Query("name"), c.Query("excludeUid"))
|
query := searchLibraryPanelsQuery{
|
||||||
|
perPage: c.QueryInt("perPage"),
|
||||||
|
page: c.QueryInt("page"),
|
||||||
|
searchString: c.Query("searchString"),
|
||||||
|
sortDirection: c.Query("sortDirection"),
|
||||||
|
panelFilter: c.Query("panelFilter"),
|
||||||
|
excludeUID: c.Query("excludeUid"),
|
||||||
|
}
|
||||||
|
libraryPanels, err := lps.getAllLibraryPanels(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toLibraryPanelError(err, "Failed to get library panels")
|
return toLibraryPanelError(err, "Failed to get library panels")
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package librarypanels
|
package librarypanels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -15,7 +18,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
sqlStatmentLibrayPanelDTOWithMeta = `
|
sqlStatmentLibrayPanelDTOWithMeta = `
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
lp.id, lp.org_id, lp.folder_id, lp.uid, lp.name, lp.type, lp.description, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by, lp.version
|
lp.name, lp.id, lp.org_id, lp.folder_id, lp.uid, lp.type, lp.description, lp.model, lp.created, lp.created_by, lp.updated, lp.updated_by, lp.version
|
||||||
, 0 AS can_edit
|
, 0 AS can_edit
|
||||||
, u1.login AS created_by_name
|
, u1.login AS created_by_name
|
||||||
, u1.email AS created_by_email
|
, u1.email AS created_by_email
|
||||||
@ -384,42 +387,71 @@ func (lps *LibraryPanelService) getLibraryPanel(c *models.ReqContext, uid string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getAllLibraryPanels gets all library panels.
|
// getAllLibraryPanels gets all library panels.
|
||||||
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, perPage int, page int, name string, excludeUID string) (LibraryPanelSearchResult, error) {
|
func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query searchLibraryPanelsQuery) (LibraryPanelSearchResult, error) {
|
||||||
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
libraryPanels := make([]LibraryPanelWithMeta, 0)
|
||||||
result := LibraryPanelSearchResult{}
|
result := LibraryPanelSearchResult{}
|
||||||
if perPage <= 0 {
|
if query.perPage <= 0 {
|
||||||
perPage = 100
|
query.perPage = 100
|
||||||
}
|
}
|
||||||
if page <= 0 {
|
if query.page <= 0 {
|
||||||
page = 1
|
query.page = 1
|
||||||
|
}
|
||||||
|
var panelFilter []string
|
||||||
|
if len(strings.TrimSpace(query.panelFilter)) > 0 {
|
||||||
|
panelFilter = strings.Split(query.panelFilter, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||||
builder := sqlstore.SQLBuilder{}
|
builder := sqlstore.SQLBuilder{}
|
||||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||||
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
||||||
if len(strings.TrimSpace(name)) > 0 {
|
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||||
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
|
builder.Write(" OR lp.description "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
}
|
}
|
||||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||||
builder.Write(" AND lp.uid <> ?", excludeUID)
|
builder.Write(" AND lp.uid <> ?", query.excludeUID)
|
||||||
|
}
|
||||||
|
if len(panelFilter) > 0 {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
sql.WriteString(` AND lp.type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")")
|
||||||
|
for _, v := range panelFilter {
|
||||||
|
params = append(params, v)
|
||||||
|
}
|
||||||
|
builder.Write(sql.String(), params...)
|
||||||
}
|
}
|
||||||
builder.Write(" UNION ")
|
builder.Write(" UNION ")
|
||||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||||
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0")
|
builder.Write(" INNER JOIN dashboard AS dashboard on lp.folder_id = dashboard.id AND lp.folder_id<>0")
|
||||||
builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
builder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
||||||
if len(strings.TrimSpace(name)) > 0 {
|
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||||
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
|
builder.Write(" OR lp.description "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
}
|
}
|
||||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||||
builder.Write(" AND lp.uid <> ?", excludeUID)
|
builder.Write(" AND lp.uid <> ?", query.excludeUID)
|
||||||
|
}
|
||||||
|
if len(panelFilter) > 0 {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
sql.WriteString(` AND lp.type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")")
|
||||||
|
for _, v := range panelFilter {
|
||||||
|
params = append(params, v)
|
||||||
|
}
|
||||||
|
builder.Write(sql.String(), params...)
|
||||||
}
|
}
|
||||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||||
}
|
}
|
||||||
if perPage != 0 {
|
if query.sortDirection == search.SortAlphaDesc.Name {
|
||||||
offset := perPage * (page - 1)
|
builder.Write(" ORDER BY 1 DESC")
|
||||||
builder.Write(lps.SQLStore.Dialect.LimitOffset(int64(perPage), int64(offset)))
|
} else {
|
||||||
|
builder.Write(" ORDER BY 1 ASC")
|
||||||
|
}
|
||||||
|
if query.perPage != 0 {
|
||||||
|
offset := query.perPage * (query.page - 1)
|
||||||
|
builder.Write(lps.SQLStore.Dialect.LimitOffset(int64(query.perPage), int64(offset)))
|
||||||
}
|
}
|
||||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -460,11 +492,21 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, perPag
|
|||||||
countBuilder := sqlstore.SQLBuilder{}
|
countBuilder := sqlstore.SQLBuilder{}
|
||||||
countBuilder.Write("SELECT * FROM library_panel")
|
countBuilder.Write("SELECT * FROM library_panel")
|
||||||
countBuilder.Write(` WHERE org_id=?`, c.SignedInUser.OrgId)
|
countBuilder.Write(` WHERE org_id=?`, c.SignedInUser.OrgId)
|
||||||
if len(strings.TrimSpace(name)) > 0 {
|
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||||
countBuilder.Write(" AND name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+name+"%")
|
countBuilder.Write(" AND name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
|
countBuilder.Write(" OR description "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||||
}
|
}
|
||||||
if len(strings.TrimSpace(excludeUID)) > 0 {
|
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||||
countBuilder.Write(" AND uid <> ?", excludeUID)
|
countBuilder.Write(" AND uid <> ?", query.excludeUID)
|
||||||
|
}
|
||||||
|
if len(panelFilter) > 0 {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
sql.WriteString(` AND type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")")
|
||||||
|
for _, v := range panelFilter {
|
||||||
|
params = append(params, v)
|
||||||
|
}
|
||||||
|
countBuilder.Write(sql.String(), params...)
|
||||||
}
|
}
|
||||||
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
|
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -473,8 +515,8 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, perPag
|
|||||||
result = LibraryPanelSearchResult{
|
result = LibraryPanelSearchResult{
|
||||||
TotalCount: int64(len(panels)),
|
TotalCount: int64(len(panels)),
|
||||||
LibraryPanels: retDTOs,
|
LibraryPanels: retDTOs,
|
||||||
Page: page,
|
Page: query.page,
|
||||||
PerPage: perPage,
|
PerPage: query.perPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -124,6 +126,252 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and sort desc is set, it should succeed and the result should be correct",
|
||||||
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||||
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
err := sc.reqContext.Req.ParseForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sc.reqContext.Req.Form.Add("sortDirection", search.SortAlphaDesc.Name)
|
||||||
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
var result libraryPanelsSearch
|
||||||
|
err = json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var expected = libraryPanelsSearch{
|
||||||
|
Result: libraryPanelsSearchResult{
|
||||||
|
TotalCount: 2,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 100,
|
||||||
|
LibraryPanels: []libraryPanel{
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[0].UID,
|
||||||
|
Name: "Text - Library Panel2",
|
||||||
|
Type: "text",
|
||||||
|
Description: "A description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "A description",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Text - Library Panel2",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[1].UID,
|
||||||
|
Name: "Text - Library Panel",
|
||||||
|
Type: "text",
|
||||||
|
Description: "A description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "A description",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Text - Library Panel",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[1].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[1].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||||
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and panelFilter is set to existing types, it should succeed and the result should be correct",
|
||||||
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", []byte(`
|
||||||
|
{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": 1,
|
||||||
|
"title": "Gauge - Library Panel",
|
||||||
|
"type": "gauge",
|
||||||
|
"description": "Gauge description"
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
command = getCreateCommandWithModel(sc.folder.Id, "BarGauge - Library Panel", []byte(`
|
||||||
|
{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": 1,
|
||||||
|
"title": "BarGauge - Library Panel",
|
||||||
|
"type": "bargauge",
|
||||||
|
"description": "BarGauge description"
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
resp = sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
err := sc.reqContext.Req.ParseForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sc.reqContext.Req.Form.Add("panelFilter", "bargauge,gauge")
|
||||||
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
var result libraryPanelsSearch
|
||||||
|
err = json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var expected = libraryPanelsSearch{
|
||||||
|
Result: libraryPanelsSearchResult{
|
||||||
|
TotalCount: 2,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 100,
|
||||||
|
LibraryPanels: []libraryPanel{
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[0].UID,
|
||||||
|
Name: "BarGauge - Library Panel",
|
||||||
|
Type: "bargauge",
|
||||||
|
Description: "BarGauge description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "BarGauge description",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "BarGauge - Library Panel",
|
||||||
|
"type": "bargauge",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[1].UID,
|
||||||
|
Name: "Gauge - Library Panel",
|
||||||
|
Type: "gauge",
|
||||||
|
Description: "Gauge description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Gauge - Library Panel",
|
||||||
|
"type": "gauge",
|
||||||
|
"description": "Gauge description",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[1].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[1].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||||
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and panelFilter is set to non existing type, it should succeed and the result should be correct",
|
||||||
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", []byte(`
|
||||||
|
{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": 1,
|
||||||
|
"title": "Gauge - Library Panel",
|
||||||
|
"type": "gauge",
|
||||||
|
"description": "Gauge description"
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
err := sc.reqContext.Req.ParseForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sc.reqContext.Req.Form.Add("panelFilter", "unknown1,unknown2")
|
||||||
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
var result libraryPanelsSearch
|
||||||
|
err = json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var expected = libraryPanelsSearch{
|
||||||
|
Result: libraryPanelsSearchResult{
|
||||||
|
TotalCount: 0,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 100,
|
||||||
|
LibraryPanels: []libraryPanel{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||||
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct",
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||||
@ -311,7 +559,182 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and name is panel2, it should succeed and the result should be correct",
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct",
|
||||||
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
command := getCreateCommandWithModel(sc.folder.Id, "Text - Library Panel2", []byte(`
|
||||||
|
{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": 1,
|
||||||
|
"title": "Text - Library Panel",
|
||||||
|
"type": "text",
|
||||||
|
"description": "Some other d e s c r i p t i o n"
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
err := sc.reqContext.Req.ParseForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||||
|
sc.reqContext.Req.Form.Add("page", "1")
|
||||||
|
sc.reqContext.Req.Form.Add("searchString", "description")
|
||||||
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
var result libraryPanelsSearch
|
||||||
|
err = json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var expected = libraryPanelsSearch{
|
||||||
|
Result: libraryPanelsSearchResult{
|
||||||
|
TotalCount: 1,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 1,
|
||||||
|
LibraryPanels: []libraryPanel{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[0].UID,
|
||||||
|
Name: "Text - Library Panel",
|
||||||
|
Type: "text",
|
||||||
|
Description: "A description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "A description",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Text - Library Panel",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||||
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct",
|
||||||
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
command := getCreateCommandWithModel(sc.folder.Id, "Some Other", []byte(`
|
||||||
|
{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"id": 1,
|
||||||
|
"title": "Text - Library Panel",
|
||||||
|
"type": "text",
|
||||||
|
"description": "A Library Panel"
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
err := sc.reqContext.Req.ParseForm()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sc.reqContext.Req.Form.Add("searchString", "Library Panel")
|
||||||
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
var result libraryPanelsSearch
|
||||||
|
err = json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var expected = libraryPanelsSearch{
|
||||||
|
Result: libraryPanelsSearchResult{
|
||||||
|
TotalCount: 2,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 100,
|
||||||
|
LibraryPanels: []libraryPanel{
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[0].UID,
|
||||||
|
Name: "Some Other",
|
||||||
|
Type: "text",
|
||||||
|
Description: "A Library Panel",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "A Library Panel",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Some Other",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[0].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[0].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
OrgID: 1,
|
||||||
|
FolderID: 1,
|
||||||
|
UID: result.Result.LibraryPanels[1].UID,
|
||||||
|
Name: "Text - Library Panel",
|
||||||
|
Type: "text",
|
||||||
|
Description: "A description",
|
||||||
|
Model: map[string]interface{}{
|
||||||
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
|
"description": "A description",
|
||||||
|
"id": float64(1),
|
||||||
|
"title": "Text - Library Panel",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Meta: LibraryPanelDTOMeta{
|
||||||
|
CanEdit: true,
|
||||||
|
ConnectedDashboards: 0,
|
||||||
|
Created: result.Result.LibraryPanels[1].Meta.Created,
|
||||||
|
Updated: result.Result.LibraryPanels[1].Meta.Updated,
|
||||||
|
CreatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
UpdatedBy: LibraryPanelDTOMetaUser{
|
||||||
|
ID: 1,
|
||||||
|
Name: UserInDbName,
|
||||||
|
AvatarUrl: UserInDbAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
|
||||||
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and searchString is panel2, it should succeed and the result should be correct",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||||
resp := sc.service.createHandler(sc.reqContext, command)
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
@ -321,7 +744,7 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||||
sc.reqContext.Req.Form.Add("page", "1")
|
sc.reqContext.Req.Form.Add("page", "1")
|
||||||
sc.reqContext.Req.Form.Add("name", "panel2")
|
sc.reqContext.Req.Form.Add("searchString", "panel2")
|
||||||
resp = sc.service.getAllHandler(sc.reqContext)
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
require.Equal(t, 200, resp.Status())
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
@ -375,7 +798,7 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and name is panel, it should succeed and the result should be correct",
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString is panel, it should succeed and the result should be correct",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||||
resp := sc.service.createHandler(sc.reqContext, command)
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
@ -385,7 +808,7 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||||
sc.reqContext.Req.Form.Add("page", "3")
|
sc.reqContext.Req.Form.Add("page", "3")
|
||||||
sc.reqContext.Req.Form.Add("name", "panel")
|
sc.reqContext.Req.Form.Add("searchString", "panel")
|
||||||
resp = sc.service.getAllHandler(sc.reqContext)
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
require.Equal(t, 200, resp.Status())
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
@ -405,7 +828,7 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and name does not exist, it should succeed and the result should be correct",
|
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString does not exist, it should succeed and the result should be correct",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
command := getCreateCommand(sc.folder.Id, "Text - Library Panel2")
|
||||||
resp := sc.service.createHandler(sc.reqContext, command)
|
resp := sc.service.createHandler(sc.reqContext, command)
|
||||||
@ -415,7 +838,7 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sc.reqContext.Req.Form.Add("perPage", "1")
|
sc.reqContext.Req.Form.Add("perPage", "1")
|
||||||
sc.reqContext.Req.Form.Add("page", "3")
|
sc.reqContext.Req.Form.Add("page", "3")
|
||||||
sc.reqContext.Req.Form.Add("name", "monkey")
|
sc.reqContext.Req.Form.Add("searchString", "monkey")
|
||||||
resp = sc.service.getAllHandler(sc.reqContext)
|
resp = sc.service.getAllHandler(sc.reqContext)
|
||||||
require.Equal(t, 200, resp.Status())
|
require.Equal(t, 200, resp.Status())
|
||||||
|
|
||||||
|
@ -720,10 +720,7 @@ func overrideLibraryPanelServiceInRegistry(cfg *setting.Cfg) LibraryPanelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCreateCommand(folderID int64, name string) createLibraryPanelCommand {
|
func getCreateCommand(folderID int64, name string) createLibraryPanelCommand {
|
||||||
command := createLibraryPanelCommand{
|
command := getCreateCommandWithModel(folderID, name, []byte(`
|
||||||
FolderID: folderID,
|
|
||||||
Name: name,
|
|
||||||
Model: []byte(`
|
|
||||||
{
|
{
|
||||||
"datasource": "${DS_GDEV-TESTDATA}",
|
"datasource": "${DS_GDEV-TESTDATA}",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@ -731,7 +728,16 @@ func getCreateCommand(folderID int64, name string) createLibraryPanelCommand {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"description": "A description"
|
"description": "A description"
|
||||||
}
|
}
|
||||||
`),
|
`))
|
||||||
|
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCreateCommandWithModel(folderID int64, name string, model []byte) createLibraryPanelCommand {
|
||||||
|
command := createLibraryPanelCommand{
|
||||||
|
FolderID: folderID,
|
||||||
|
Name: name,
|
||||||
|
Model: model,
|
||||||
}
|
}
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
@ -137,3 +137,13 @@ type patchLibraryPanelCommand struct {
|
|||||||
Model json.RawMessage `json:"model"`
|
Model json.RawMessage `json:"model"`
|
||||||
Version int64 `json:"version" binding:"Required"`
|
Version int64 `json:"version" binding:"Required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchLibraryPanelsQuery is the query used for searching for LibraryPanels
|
||||||
|
type searchLibraryPanelsQuery struct {
|
||||||
|
perPage int
|
||||||
|
page int
|
||||||
|
searchString string
|
||||||
|
sortDirection string
|
||||||
|
panelFilter string
|
||||||
|
excludeUID string
|
||||||
|
}
|
||||||
|
@ -60,8 +60,8 @@ type SearchService struct {
|
|||||||
func (s *SearchService) Init() error {
|
func (s *SearchService) Init() error {
|
||||||
s.Bus.AddHandler(s.searchHandler)
|
s.Bus.AddHandler(s.searchHandler)
|
||||||
s.sortOptions = map[string]SortOption{
|
s.sortOptions = map[string]SortOption{
|
||||||
sortAlphaAsc.Name: sortAlphaAsc,
|
SortAlphaAsc.Name: SortAlphaAsc,
|
||||||
sortAlphaDesc.Name: sortAlphaDesc,
|
SortAlphaDesc.Name: SortAlphaDesc,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sortAlphaAsc = SortOption{
|
SortAlphaAsc = SortOption{
|
||||||
Name: "alpha-asc",
|
Name: "alpha-asc",
|
||||||
DisplayName: "Alphabetically (A–Z)",
|
DisplayName: "Alphabetically (A–Z)",
|
||||||
Description: "Sort results in an alphabetically ascending order",
|
Description: "Sort results in an alphabetically ascending order",
|
||||||
@ -16,7 +16,7 @@ var (
|
|||||||
searchstore.TitleSorter{},
|
searchstore.TitleSorter{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
sortAlphaDesc = SortOption{
|
SortAlphaDesc = SortOption{
|
||||||
Name: "alpha-desc",
|
Name: "alpha-desc",
|
||||||
DisplayName: "Alphabetically (Z–A)",
|
DisplayName: "Alphabetically (Z–A)",
|
||||||
Description: "Sort results in an alphabetically descending order",
|
Description: "Sort results in an alphabetically descending order",
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { GrafanaThemeV2, PanelPluginMeta, SelectableValue } from '@grafana/data';
|
||||||
|
import { getAllPanelPluginMeta } from '../../../features/dashboard/components/VizTypePicker/VizTypePicker';
|
||||||
|
import { Icon, resetSelectStyles, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (plugins: PanelPluginMeta[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelTypeFilter = ({ onChange: propsOnChange }: Props): JSX.Element => {
|
||||||
|
const plugins = useMemo<PanelPluginMeta[]>(() => {
|
||||||
|
return getAllPanelPluginMeta();
|
||||||
|
}, []);
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
plugins
|
||||||
|
.map((p) => ({ label: p.name, imgUrl: p.info.logos.small, value: p }))
|
||||||
|
.sort((a, b) => a.label?.localeCompare(b.label)),
|
||||||
|
[plugins]
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState<Array<SelectableValue<PanelPluginMeta>>>([]);
|
||||||
|
const onChange = useCallback(
|
||||||
|
(plugins: Array<SelectableValue<PanelPluginMeta>>) => {
|
||||||
|
const changedPlugins = [];
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (plugin.value) {
|
||||||
|
changedPlugins.push(plugin.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propsOnChange(changedPlugins);
|
||||||
|
setValue(plugins);
|
||||||
|
},
|
||||||
|
[propsOnChange]
|
||||||
|
);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const selectOptions = {
|
||||||
|
defaultOptions: true,
|
||||||
|
getOptionLabel: (i: any) => i.label,
|
||||||
|
getOptionValue: (i: any) => i.value,
|
||||||
|
isMulti: true,
|
||||||
|
noOptionsMessage: 'No Panel types found',
|
||||||
|
placeholder: 'Filter by Panel type',
|
||||||
|
styles: resetSelectStyles(),
|
||||||
|
maxMenuHeight: 150,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<span className={styles.clear} onClick={() => onChange([])}>
|
||||||
|
Clear types
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Select {...selectOptions} prefix={<Icon name="table" />} aria-label="Panel Type filter" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaThemeV2) {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
label: container;
|
||||||
|
position: relative;
|
||||||
|
min-width: 180px;
|
||||||
|
flex-grow: 1;
|
||||||
|
`,
|
||||||
|
clear: css`
|
||||||
|
label: clear;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: ${theme.spacing(1.5)};
|
||||||
|
position: absolute;
|
||||||
|
top: -${theme.spacing(2.75)};
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.colors.text.link};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.text.maxContrast};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { connect, MapDispatchToProps } from 'react-redux';
|
|
||||||
import { css, cx, keyframes } from '@emotion/css';
|
import { css, cx, keyframes } from '@emotion/css';
|
||||||
import { chain, cloneDeep, defaults, find, sortBy } from 'lodash';
|
import { chain, cloneDeep, defaults, find, sortBy } from 'lodash';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
@ -10,13 +9,13 @@ import { GrafanaTheme } from '@grafana/data';
|
|||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
|
||||||
import { addPanel } from 'app/features/dashboard/state/reducers';
|
import { addPanel } from 'app/features/dashboard/state/reducers';
|
||||||
import { DashboardModel, PanelModel } from '../../state';
|
import { DashboardModel, PanelModel } from '../../state';
|
||||||
import { LibraryPanelsView } from '../../../library-panels/components/LibraryPanelsView/LibraryPanelsView';
|
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
import { LibraryPanelDTO } from '../../../library-panels/types';
|
import { LibraryPanelDTO } from '../../../library-panels/types';
|
||||||
import { toPanelModelLibraryPanel } from '../../../library-panels/utils';
|
import { toPanelModelLibraryPanel } from '../../../library-panels/utils';
|
||||||
|
import { LibraryPanelsSearch } from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
|
||||||
|
import { connect, MapDispatchToProps } from 'react-redux';
|
||||||
|
|
||||||
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
|
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
|
||||||
|
|
||||||
@ -56,7 +55,6 @@ const getCopiedPanelPlugins = () => {
|
|||||||
|
|
||||||
export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard }) => {
|
export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard }) => {
|
||||||
const [addPanelView, setAddPanelView] = useState(false);
|
const [addPanelView, setAddPanelView] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
|
|
||||||
const onCancelAddPanel = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
const onCancelAddPanel = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -140,17 +138,7 @@ export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard })
|
|||||||
{addPanelView ? 'Add panel from panel library' : 'Add panel'}
|
{addPanelView ? 'Add panel from panel library' : 'Add panel'}
|
||||||
</AddPanelWidgetHandle>
|
</AddPanelWidgetHandle>
|
||||||
{addPanelView ? (
|
{addPanelView ? (
|
||||||
<>
|
<LibraryPanelsSearch onClick={onAddLibraryPanel} perPage={3} />
|
||||||
<div className={styles.panelSearchInput}>
|
|
||||||
<FilterInput width={0} value={searchQuery} onChange={setSearchQuery} placeholder={'Search global panels'} />
|
|
||||||
</div>
|
|
||||||
<LibraryPanelsView
|
|
||||||
className={styles.libraryPanelsWrapper}
|
|
||||||
onClickCard={(panel) => onAddLibraryPanel(panel)}
|
|
||||||
showSecondaryActions={false}
|
|
||||||
searchString={searchQuery}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.actionsWrapper}>
|
<div className={styles.actionsWrapper}>
|
||||||
<div className={styles.actionsRow}>
|
<div className={styles.actionsRow}>
|
||||||
@ -226,10 +214,6 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
panelSearchInput: css`
|
|
||||||
padding-left: ${theme.spacing.sm};
|
|
||||||
padding-right: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
outline: 2px dotted transparent;
|
outline: 2px dotted transparent;
|
||||||
@ -272,9 +256,6 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
padding: 0 ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
|
padding: 0 ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
libraryPanelsWrapper: css`
|
|
||||||
padding: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
headerRow: css`
|
headerRow: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -12,9 +12,7 @@ exports[`Render should render component 1`] = `
|
|||||||
"actionsWrapper": "css-gxxmom",
|
"actionsWrapper": "css-gxxmom",
|
||||||
"backButton": "css-1cdxa9p",
|
"backButton": "css-1cdxa9p",
|
||||||
"headerRow": "css-3sdqvi",
|
"headerRow": "css-3sdqvi",
|
||||||
"libraryPanelsWrapper": "css-18m13of",
|
|
||||||
"noMargin": "css-u023fv",
|
"noMargin": "css-u023fv",
|
||||||
"panelSearchInput": "css-2ug8g3",
|
|
||||||
"wrapper": "css-e4b3m6",
|
"wrapper": "css-e4b3m6",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
|
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
|
||||||
import { StoreState } from '../../types';
|
import { StoreState } from '../../types';
|
||||||
import { getNavModel } from '../../core/selectors/navModel';
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
import Page from '../../core/components/Page/Page';
|
import Page from '../../core/components/Page/Page';
|
||||||
import { LibraryPanelsView } from './components/LibraryPanelsView/LibraryPanelsView';
|
import { LibraryPanelsSearch } from './components/LibraryPanelsSearch/LibraryPanelsSearch';
|
||||||
import { useAsync } from 'react-use';
|
|
||||||
import { getLibraryPanels } from './state/api';
|
|
||||||
import PageActionBar from '../../core/components/PageActionBar/PageActionBar';
|
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
navModel: getNavModel(state.navIndex, 'library-panels'),
|
navModel: getNavModel(state.navIndex, 'library-panels'),
|
||||||
@ -22,28 +18,15 @@ interface OwnProps extends GrafanaRouteComponentProps {}
|
|||||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
export const LibraryPanelsPage: FC<Props> = ({ navModel }) => {
|
export const LibraryPanelsPage: FC<Props> = ({ navModel }) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const { value: searchResult, loading } = useAsync(async () => {
|
|
||||||
return getLibraryPanels();
|
|
||||||
});
|
|
||||||
const hasLibraryPanels = Boolean(searchResult?.libraryPanels.length);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel}>
|
<Page navModel={navModel}>
|
||||||
<Page.Contents isLoading={loading}>
|
<Page.Contents>
|
||||||
{hasLibraryPanels && (
|
<LibraryPanelsSearch onClick={noop} showSecondaryActions showSort showFilter />
|
||||||
<PageActionBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} placeholder={'Search by name'} />
|
|
||||||
)}
|
|
||||||
<LibraryPanelsView
|
|
||||||
onClickCard={() => undefined}
|
|
||||||
searchString={searchQuery}
|
|
||||||
currentPanelId={undefined}
|
|
||||||
showSecondaryActions={true}
|
|
||||||
perPage={DEFAULT_PER_PAGE_PAGINATION}
|
|
||||||
/>
|
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(LibraryPanelsPage);
|
export default connect(mapStateToProps)(LibraryPanelsPage);
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { HorizontalGroup, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||||
|
import { GrafanaThemeV2, PanelPluginMeta, SelectableValue } from '@grafana/data';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { FilterInput } from '../../../../core/components/FilterInput/FilterInput';
|
||||||
|
import { SortPicker } from '../../../../core/components/Select/SortPicker';
|
||||||
|
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
|
||||||
|
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
|
||||||
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants';
|
||||||
|
import { LibraryPanelDTO } from '../../types';
|
||||||
|
|
||||||
|
export interface LibraryPanelsSearchProps {
|
||||||
|
onClick: (panel: LibraryPanelDTO) => void;
|
||||||
|
showSort?: boolean;
|
||||||
|
showFilter?: boolean;
|
||||||
|
showSecondaryActions?: boolean;
|
||||||
|
currentPanelId?: string;
|
||||||
|
perPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LibraryPanelsSearch = ({
|
||||||
|
onClick,
|
||||||
|
currentPanelId,
|
||||||
|
perPage = DEFAULT_PER_PAGE_PAGINATION,
|
||||||
|
showFilter = false,
|
||||||
|
showSort = false,
|
||||||
|
showSecondaryActions = false,
|
||||||
|
}: LibraryPanelsSearchProps): JSX.Element => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [sortDirection, setSortDirection] = useState<string | undefined>(undefined);
|
||||||
|
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const onSortChange = useCallback((sort: SelectableValue<string>) => setSortDirection(sort.value), []);
|
||||||
|
const onFilterChange = useCallback((plugins: PanelPluginMeta[]) => setPanelFilter(plugins.map((p) => p.id)), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<VerticalGroup spacing={showSort || showFilter ? 'lg' : 'xs'}>
|
||||||
|
<FilterInput value={searchQuery} onChange={setSearchQuery} placeholder={'Search by name'} width={0} />
|
||||||
|
<HorizontalGroup spacing="sm" justify={showSort && showFilter ? 'space-between' : 'flex-end'}>
|
||||||
|
{showSort && <SortPicker value={sortDirection} onChange={onSortChange} />}
|
||||||
|
{showFilter && <PanelTypeFilter onChange={onFilterChange} />}
|
||||||
|
</HorizontalGroup>
|
||||||
|
<div className={styles.libraryPanelsView}>
|
||||||
|
<LibraryPanelsView
|
||||||
|
onClickCard={onClick}
|
||||||
|
searchString={searchQuery}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
panelFilter={panelFilter}
|
||||||
|
currentPanelId={currentPanelId}
|
||||||
|
showSecondaryActions={showSecondaryActions}
|
||||||
|
perPage={perPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VerticalGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaThemeV2) {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
libraryPanelsView: css`
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
@ -15,6 +15,8 @@ interface LibraryPanelViewProps {
|
|||||||
showSecondaryActions?: boolean;
|
showSecondaryActions?: boolean;
|
||||||
currentPanelId?: string;
|
currentPanelId?: string;
|
||||||
searchString: string;
|
searchString: string;
|
||||||
|
sortDirection?: string;
|
||||||
|
panelFilter?: string[];
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
|||||||
className,
|
className,
|
||||||
onClickCard,
|
onClickCard,
|
||||||
searchString,
|
searchString,
|
||||||
|
sortDirection,
|
||||||
|
panelFilter,
|
||||||
showSecondaryActions,
|
showSecondaryActions,
|
||||||
currentPanelId: currentPanel,
|
currentPanelId: currentPanel,
|
||||||
perPage: propsPerPage = 40,
|
perPage: propsPerPage = 40,
|
||||||
@ -36,11 +40,14 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
|
||||||
useDebounce(() => asyncDispatch(searchForLibraryPanels({ searchString, page, perPage, currentPanelId })), 300, [
|
useDebounce(
|
||||||
searchString,
|
() =>
|
||||||
page,
|
asyncDispatch(
|
||||||
asyncDispatch,
|
searchForLibraryPanels({ searchString, sortDirection, panelFilter, page, perPage, currentPanelId })
|
||||||
]);
|
),
|
||||||
|
300,
|
||||||
|
[searchString, sortDirection, panelFilter, page, asyncDispatch]
|
||||||
|
);
|
||||||
const onDelete = ({ uid }: LibraryPanelDTO) =>
|
const onDelete = ({ uid }: LibraryPanelDTO) =>
|
||||||
asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
|
asyncDispatch(deleteLibraryPanel(uid, { searchString, page, perPage }));
|
||||||
const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
|
const onPageChange = (page: number) => asyncDispatch(changePage({ page }));
|
||||||
@ -51,7 +58,7 @@ export const LibraryPanelsView: React.FC<LibraryPanelViewProps> = ({
|
|||||||
{loadingState === LoadingState.Loading ? (
|
{loadingState === LoadingState.Loading ? (
|
||||||
<p>Loading library panels...</p>
|
<p>Loading library panels...</p>
|
||||||
) : libraryPanels.length < 1 ? (
|
) : libraryPanels.length < 1 ? (
|
||||||
<p>No library panels found.</p>
|
<p className={styles.noPanelsFound}>No library panels found.</p>
|
||||||
) : (
|
) : (
|
||||||
libraryPanels?.map((item, i) => (
|
libraryPanels?.map((item, i) => (
|
||||||
<LibraryPanelCard
|
<LibraryPanelCard
|
||||||
@ -101,5 +108,9 @@ const getPanelViewStyles = (theme: GrafanaTheme) => {
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
margin-top: ${theme.spacing.sm};
|
margin-top: ${theme.spacing.sm};
|
||||||
`,
|
`,
|
||||||
|
noPanelsFound: css`
|
||||||
|
label: noPanelsFound;
|
||||||
|
min-height: 200px;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,8 @@ interface SearchArgs {
|
|||||||
perPage: number;
|
perPage: number;
|
||||||
page: number;
|
page: number;
|
||||||
searchString: string;
|
searchString: string;
|
||||||
|
sortDirection?: string;
|
||||||
|
panelFilter?: string[];
|
||||||
currentPanelId?: string;
|
currentPanelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,10 +21,12 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
|
|||||||
const subscription = new Subscription();
|
const subscription = new Subscription();
|
||||||
const dataObservable = from(
|
const dataObservable = from(
|
||||||
getLibraryPanels({
|
getLibraryPanels({
|
||||||
name: args.searchString,
|
searchString: args.searchString,
|
||||||
perPage: args.perPage,
|
perPage: args.perPage,
|
||||||
page: args.page,
|
page: args.page,
|
||||||
excludeUid: args.currentPanelId,
|
excludeUid: args.currentPanelId,
|
||||||
|
sortDirection: args.sortDirection,
|
||||||
|
panelFilter: args.panelFilter,
|
||||||
})
|
})
|
||||||
).pipe(
|
).pipe(
|
||||||
mergeMap(({ perPage, libraryPanels, page, totalCount }) =>
|
mergeMap(({ perPage, libraryPanels, page, totalCount }) =>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaThemeV2, PanelPluginMeta } from '@grafana/data';
|
||||||
import { Button, useStyles } from '@grafana/ui';
|
import { Button, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||||
|
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
|
import { AddLibraryPanelModal } from '../AddLibraryPanelModal/AddLibraryPanelModal';
|
||||||
@ -13,6 +13,7 @@ import { toPanelModelLibraryPanel } from '../../utils';
|
|||||||
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
|
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { ChangeLibraryPanelModal } from '../ChangeLibraryPanelModal/ChangeLibraryPanelModal';
|
import { ChangeLibraryPanelModal } from '../ChangeLibraryPanelModal/ChangeLibraryPanelModal';
|
||||||
|
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -20,9 +21,16 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
|
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
|
||||||
const [changeToPanel, setChangeToPanel] = useState<LibraryPanelDTO | undefined>(undefined);
|
const [changeToPanel, setChangeToPanel] = useState<LibraryPanelDTO | undefined>(undefined);
|
||||||
|
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||||
|
const onPanelFilterChange = useCallback(
|
||||||
|
(plugins: PanelPluginMeta[]) => {
|
||||||
|
setPanelFilter(plugins.map((p) => p.id));
|
||||||
|
},
|
||||||
|
[setPanelFilter]
|
||||||
|
);
|
||||||
const dashboard = getDashboardSrv().getCurrent();
|
const dashboard = getDashboardSrv().getCurrent();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@ -64,21 +72,26 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.box}>
|
<VerticalGroup spacing="md">
|
||||||
{!panel.libraryPanel && (
|
{!panel.libraryPanel && (
|
||||||
<div className={styles.addButtonWrapper}>
|
<VerticalGroup align="center">
|
||||||
<Button icon="plus" onClick={onAddToPanelLibrary} variant="secondary" fullWidth>
|
<Button icon="plus" onClick={onAddToPanelLibrary} variant="secondary" fullWidth>
|
||||||
Add current panel to library
|
Add current panel to library
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</VerticalGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LibraryPanelsView
|
<PanelTypeFilter onChange={onPanelFilterChange} />
|
||||||
currentPanelId={panel.libraryPanel?.uid}
|
|
||||||
searchString={searchQuery}
|
<div className={styles.libraryPanelsView}>
|
||||||
onClickCard={onChangeLibraryPanel}
|
<LibraryPanelsView
|
||||||
showSecondaryActions
|
currentPanelId={panel.libraryPanel?.uid}
|
||||||
/>
|
searchString={searchQuery}
|
||||||
|
panelFilter={panelFilter}
|
||||||
|
onClickCard={onChangeLibraryPanel}
|
||||||
|
showSecondaryActions
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{showingAddPanelModal && (
|
{showingAddPanelModal && (
|
||||||
<AddLibraryPanelModal
|
<AddLibraryPanelModal
|
||||||
@ -92,20 +105,14 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
|||||||
{changeToPanel && (
|
{changeToPanel && (
|
||||||
<ChangeLibraryPanelModal panel={panel} onDismiss={onDismissChangeToPanel} onConfirm={useLibraryPanel} />
|
<ChangeLibraryPanelModal panel={panel} onDismiss={onDismissChangeToPanel} onConfirm={useLibraryPanel} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</VerticalGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => {
|
const getStyles = (theme: GrafanaThemeV2) => {
|
||||||
return {
|
return {
|
||||||
box: css``,
|
libraryPanelsView: css`
|
||||||
addButtonWrapper: css`
|
width: 100%;
|
||||||
padding-bottom: ${theme.spacing.md};
|
|
||||||
text-align: center;
|
|
||||||
`,
|
|
||||||
panelLibraryTitle: css`
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,20 +2,26 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
|
import { LibraryPanelDTO, LibraryPanelSearchResult, PanelModelWithLibraryPanel } from '../types';
|
||||||
|
|
||||||
export interface GetLibraryPanelsOptions {
|
export interface GetLibraryPanelsOptions {
|
||||||
name?: string;
|
searchString?: string;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
excludeUid?: string;
|
excludeUid?: string;
|
||||||
|
sortDirection?: string;
|
||||||
|
panelFilter?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLibraryPanels({
|
export async function getLibraryPanels({
|
||||||
name = '',
|
searchString = '',
|
||||||
perPage = 100,
|
perPage = 100,
|
||||||
page = 1,
|
page = 1,
|
||||||
excludeUid = '',
|
excludeUid = '',
|
||||||
|
sortDirection = '',
|
||||||
|
panelFilter = [],
|
||||||
}: GetLibraryPanelsOptions = {}): Promise<LibraryPanelSearchResult> {
|
}: GetLibraryPanelsOptions = {}): Promise<LibraryPanelSearchResult> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('name', name);
|
params.append('searchString', searchString);
|
||||||
|
params.append('sortDirection', sortDirection);
|
||||||
|
params.append('panelFilter', panelFilter.join(','));
|
||||||
params.append('excludeUid', excludeUid);
|
params.append('excludeUid', excludeUid);
|
||||||
params.append('perPage', perPage.toString(10));
|
params.append('perPage', perPage.toString(10));
|
||||||
params.append('page', page.toString(10));
|
params.append('page', page.toString(10));
|
||||||
|
Loading…
Reference in New Issue
Block a user