LibraryPanels: Adds folder filter to manage library panel page (#33560)
* LibraryPanels: Adds folder filter * Refactor: Adds folder filter to library search * Refactor: splits huge function into smaller functions * LibraryPanels: Adds Panels Page to Manage Folder tabs (#33618) * Chore: adds tests to LibraryPanelsSearch * Refactor: Adds reducer and tests * Chore: changes GrafanaThemeV2 * Refactor: pulls everything behind the feature toggle * Chore: removes clear icon from FolderFilter * Chore: adds filter to SortPicker * Refactor: using useAsync instead
This commit is contained in:
+9
-6
@@ -167,12 +167,15 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
Url: hs.Cfg.AppSubURL + "/dashboard/snapshots",
|
||||
Icon: "camera",
|
||||
})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||
Text: "Library panels",
|
||||
Id: "library-panels",
|
||||
Url: hs.Cfg.AppSubURL + "/library-panels",
|
||||
Icon: "library-panel",
|
||||
})
|
||||
|
||||
if hs.Cfg.IsPanelLibraryEnabled() {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
|
||||
Text: "Library panels",
|
||||
Id: "library-panels",
|
||||
Url: hs.Cfg.AppSubURL + "/library-panels",
|
||||
Icon: "library-panel",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
|
||||
@@ -88,6 +88,7 @@ func (lps *LibraryPanelService) getAllHandler(c *models.ReqContext) response.Res
|
||||
sortDirection: c.Query("sortDirection"),
|
||||
panelFilter: c.Query("panelFilter"),
|
||||
excludeUID: c.Query("excludeUid"),
|
||||
folderFilter: c.Query("folderFilter"),
|
||||
}
|
||||
libraryPanels, err := lps.getAllLibraryPanels(c, query)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -400,46 +398,28 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query
|
||||
if len(strings.TrimSpace(query.panelFilter)) > 0 {
|
||||
panelFilter = strings.Split(query.panelFilter, ",")
|
||||
}
|
||||
|
||||
folderFilter := parseFolderFilter(query)
|
||||
if folderFilter.parseError != nil {
|
||||
return LibraryPanelSearchResult{}, folderFilter.parseError
|
||||
}
|
||||
err := lps.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
||||
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||
builder.Write(" AND lp.name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
builder.Write(" OR lp.description "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
if folderFilter.includeGeneralFolder {
|
||||
builder.Write(sqlStatmentLibrayPanelDTOWithMeta)
|
||||
builder.Write(` WHERE lp.org_id=? AND lp.folder_id=0`, c.SignedInUser.OrgId)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writePanelFilterSQL(panelFilter, &builder)
|
||||
builder.Write(" UNION ")
|
||||
}
|
||||
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||
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(sqlStatmentLibrayPanelDTOWithMeta)
|
||||
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)
|
||||
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||
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(query.excludeUID)) > 0 {
|
||||
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...)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &builder)
|
||||
writeExcludeSQL(query, &builder)
|
||||
writePanelFilterSQL(panelFilter, &builder)
|
||||
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||
@@ -449,10 +429,7 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query
|
||||
} 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)))
|
||||
}
|
||||
writePerPageSQL(query, lps.SQLStore, &builder)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryPanels); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -490,23 +467,13 @@ func (lps *LibraryPanelService) getAllLibraryPanels(c *models.ReqContext, query
|
||||
|
||||
var panels []LibraryPanel
|
||||
countBuilder := sqlstore.SQLBuilder{}
|
||||
countBuilder.Write("SELECT * FROM library_panel")
|
||||
countBuilder.Write(` WHERE org_id=?`, c.SignedInUser.OrgId)
|
||||
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||
countBuilder.Write(" AND name "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
countBuilder.Write(" OR description "+lps.SQLStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
}
|
||||
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||
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...)
|
||||
countBuilder.Write("SELECT * FROM library_panel AS lp")
|
||||
countBuilder.Write(` WHERE lp.org_id=?`, c.SignedInUser.OrgId)
|
||||
writeSearchStringSQL(query, lps.SQLStore, &countBuilder)
|
||||
writeExcludeSQL(query, &countBuilder)
|
||||
writePanelFilterSQL(panelFilter, &countBuilder)
|
||||
if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&panels); err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,6 +2,7 @@ package librarypanels
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
@@ -372,6 +373,196 @@ func TestGetAllLibraryPanels(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
scenarioWithLibraryPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to existing folders, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
command := getCreateCommand(newFolder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
folderFilter := strconv.FormatInt(newFolder.Id, 10)
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("folderFilter", folderFilter)
|
||||
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: 100,
|
||||
LibraryPanels: []libraryPanel{
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: newFolder.Id,
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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 folderFilter is set to non existing folders, it should succeed and the result should be correct",
|
||||
func(t *testing.T, sc scenarioContext) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
command := getCreateCommand(newFolder.Id, "Text - Library Panel2")
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
folderFilter := "2020,2021"
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("folderFilter", folderFilter)
|
||||
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 folderFilter is set to General folder, 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())
|
||||
folderFilter := "0"
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("folderFilter", folderFilter)
|
||||
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{
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
OrgID: 1,
|
||||
FolderID: 1,
|
||||
UID: result.Result.LibraryPanels[1].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[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 excludeUID 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")
|
||||
|
||||
@@ -146,4 +146,5 @@ type searchLibraryPanelsQuery struct {
|
||||
sortDirection string
|
||||
panelFilter string
|
||||
excludeUID string
|
||||
folderFilter string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package librarypanels
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func writePerPageSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
||||
if query.perPage != 0 {
|
||||
offset := query.perPage * (query.page - 1)
|
||||
builder.Write(sqlStore.Dialect.LimitOffset(int64(query.perPage), int64(offset)))
|
||||
}
|
||||
}
|
||||
|
||||
func writePanelFilterSQL(panelFilter []string, builder *sqlstore.SQLBuilder) {
|
||||
if len(panelFilter) > 0 {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
sql.WriteString(` AND lp.type IN (?` + strings.Repeat(",?", len(panelFilter)-1) + ")")
|
||||
for _, filter := range panelFilter {
|
||||
params = append(params, filter)
|
||||
}
|
||||
builder.Write(sql.String(), params...)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSearchStringSQL(query searchLibraryPanelsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
||||
if len(strings.TrimSpace(query.searchString)) > 0 {
|
||||
builder.Write(" AND (lp.name "+sqlStore.Dialect.LikeStr()+" ?", "%"+query.searchString+"%")
|
||||
builder.Write(" OR lp.description "+sqlStore.Dialect.LikeStr()+" ?)", "%"+query.searchString+"%")
|
||||
}
|
||||
}
|
||||
|
||||
func writeExcludeSQL(query searchLibraryPanelsQuery, builder *sqlstore.SQLBuilder) {
|
||||
if len(strings.TrimSpace(query.excludeUID)) > 0 {
|
||||
builder.Write(" AND lp.uid <> ?", query.excludeUID)
|
||||
}
|
||||
}
|
||||
|
||||
type FolderFilter struct {
|
||||
includeGeneralFolder bool
|
||||
folderIDs []string
|
||||
parseError error
|
||||
}
|
||||
|
||||
func parseFolderFilter(query searchLibraryPanelsQuery) FolderFilter {
|
||||
var folderIDs []string
|
||||
if len(strings.TrimSpace(query.folderFilter)) == 0 {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: true,
|
||||
folderIDs: folderIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
}
|
||||
|
||||
includeGeneralFolder := false
|
||||
folderIDs = strings.Split(query.folderFilter, ",")
|
||||
for _, filter := range folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: false,
|
||||
folderIDs: folderIDs,
|
||||
parseError: err,
|
||||
}
|
||||
}
|
||||
if isGeneralFolder(folderID) {
|
||||
includeGeneralFolder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return FolderFilter{
|
||||
includeGeneralFolder: includeGeneralFolder,
|
||||
folderIDs: folderIDs,
|
||||
parseError: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FolderFilter) writeFolderFilterSQL(includeGeneral bool, builder *sqlstore.SQLBuilder) error {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
for _, filter := range f.folderIDs {
|
||||
folderID, err := strconv.ParseInt(filter, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !includeGeneral && isGeneralFolder(folderID) {
|
||||
continue
|
||||
}
|
||||
params = append(params, filter)
|
||||
}
|
||||
if len(params) > 0 {
|
||||
sql.WriteString(` AND lp.folder_id IN (?` + strings.Repeat(",?", len(params)-1) + ")")
|
||||
builder.Write(sql.String(), params...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user