mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search API: Search by folder UID (#65040)
* Search: Attempt to support folderUID filter * Search: Use folder UID instead of ID for searching folders * Update swagger * Fix JSON property casing * Add integration test * Remove redundant query condition * Fix frontend test * Fix listing dashboards in General/root * Add support for fetching top level folders using `folderUIDs=` (empty string) query parameter * Add deprecation notice * Send uid of general in sql.ts * Use 'general' for query folderUIDs query param for fetching folder * Add tests * Fix FolderUIDFilter --------- Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
@@ -60,7 +60,12 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response {
|
||||
}
|
||||
}
|
||||
|
||||
if len(dbIDs) > 0 && len(dbUIDs) > 0 {
|
||||
folderUIDs := c.QueryStrings("folderUIDs")
|
||||
|
||||
bothDashboardIds := len(dbIDs) > 0 && len(dbUIDs) > 0
|
||||
bothFolderIds := len(folderIDs) > 0 && len(folderUIDs) > 0
|
||||
|
||||
if bothDashboardIds || bothFolderIds {
|
||||
return response.Error(400, "search supports UIDs or IDs, not both", nil)
|
||||
}
|
||||
|
||||
@@ -76,6 +81,7 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response {
|
||||
DashboardUIDs: dbUIDs,
|
||||
Type: dashboardType,
|
||||
FolderIds: folderIDs,
|
||||
FolderUIDs: folderUIDs,
|
||||
Permission: permission,
|
||||
Sort: sort,
|
||||
}
|
||||
@@ -136,17 +142,27 @@ type SearchParams struct {
|
||||
// Enum: dash-folder,dash-db
|
||||
Type string `json:"type"`
|
||||
// List of dashboard id’s to search for
|
||||
// This is deprecated: users should use the `dashboardUIDs` query parameter instead
|
||||
// in:query
|
||||
// required: false
|
||||
// deprecated: true
|
||||
DashboardIds []int64 `json:"dashboardIds"`
|
||||
// List of dashboard uid’s to search for
|
||||
// in:query
|
||||
// required: false
|
||||
DashboardUIDs []string `json:"dashboardUIDs"`
|
||||
// List of folder id’s to search in for dashboards
|
||||
// If it's `0` then it will query for the top level folders
|
||||
// This is deprecated: users should use the `folderUIDs` query parameter instead
|
||||
// in:query
|
||||
// required: false
|
||||
// deprecated: true
|
||||
FolderIds []int64 `json:"folderIds"`
|
||||
// List of folder UID’s to search in for dashboards
|
||||
// If it's an empty string then it will query for the top level folders
|
||||
// in:query
|
||||
// required: false
|
||||
FolderUIDs []string `json:"folderUIDs"`
|
||||
// Flag indicating if only starred Dashboards should be returned
|
||||
// in:query
|
||||
// required: false
|
||||
|
||||
@@ -975,10 +975,13 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
||||
|
||||
filters = append(filters, query.Filters...)
|
||||
|
||||
var orgID int64
|
||||
if query.OrgId != 0 {
|
||||
filters = append(filters, searchstore.OrgFilter{OrgId: query.OrgId})
|
||||
orgID = query.OrgId
|
||||
filters = append(filters, searchstore.OrgFilter{OrgId: orgID})
|
||||
} else if query.SignedInUser.OrgID != 0 {
|
||||
filters = append(filters, searchstore.OrgFilter{OrgId: query.SignedInUser.OrgID})
|
||||
orgID = query.SignedInUser.OrgID
|
||||
filters = append(filters, searchstore.OrgFilter{OrgId: orgID})
|
||||
}
|
||||
|
||||
if len(query.Tags) > 0 {
|
||||
@@ -1003,6 +1006,10 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
||||
filters = append(filters, searchstore.FolderFilter{IDs: query.FolderIds})
|
||||
}
|
||||
|
||||
if len(query.FolderUIDs) > 0 {
|
||||
filters = append(filters, searchstore.FolderUIDFilter{Dialect: d.store.GetDialect(), OrgID: orgID, UIDs: query.FolderUIDs})
|
||||
}
|
||||
|
||||
var res []dashboards.DashboardSearchProjection
|
||||
sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
@@ -451,6 +452,33 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
|
||||
require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug))
|
||||
})
|
||||
|
||||
t.Run("Should be able to find a dashboard folder's children by UID", func(t *testing.T) {
|
||||
setup()
|
||||
query := dashboards.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderUIDs: []string{savedFolder.UID},
|
||||
SignedInUser: &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleEditor,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hits, err := testSearchDashboards(dashboardStore, &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(hits), 2)
|
||||
hit := hits[0]
|
||||
require.Equal(t, hit.ID, savedDash.ID)
|
||||
require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug))
|
||||
require.Equal(t, hit.FolderID, savedFolder.ID)
|
||||
require.Equal(t, hit.FolderUID, savedFolder.UID)
|
||||
require.Equal(t, hit.FolderTitle, savedFolder.Title)
|
||||
require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug))
|
||||
})
|
||||
|
||||
t.Run("Should be able to find dashboards by ids", func(t *testing.T) {
|
||||
setup()
|
||||
query := dashboards.FindPersistedDashboardsQuery{
|
||||
@@ -674,6 +702,111 @@ func TestGetExistingDashboardByTitleAndFolder(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
|
||||
quotaService := quotatest.New(false, nil)
|
||||
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := int64(1)
|
||||
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, false)
|
||||
|
||||
f0 := insertTestDashboard(t, dashboardStore, "f0", orgID, 0, true)
|
||||
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, false)
|
||||
|
||||
f1 := insertTestDashboard(t, dashboardStore, "f1", orgID, 0, true)
|
||||
insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, false)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
folderIDs []int64
|
||||
folderUIDs []string
|
||||
expectedResult []string
|
||||
}{
|
||||
{
|
||||
desc: "find dashboard under general using folder id",
|
||||
folderIDs: []int64{0},
|
||||
expectedResult: []string{"dashboard under general"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under f0 using folder id",
|
||||
folderIDs: []int64{f0.ID},
|
||||
expectedResult: []string{"dashboard under f0"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under f0 or f1 using folder id",
|
||||
folderIDs: []int64{f0.ID, f1.ID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under f1"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under general using folder UID",
|
||||
folderUIDs: []string{folder.GeneralFolderUID},
|
||||
expectedResult: []string{"dashboard under general"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under f0 using folder UID",
|
||||
folderUIDs: []string{f0.UID},
|
||||
expectedResult: []string{"dashboard under f0"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under f0 or f1 using folder UID",
|
||||
folderUIDs: []string{f0.UID, f1.UID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under f1"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under general or f0 using folder id",
|
||||
folderIDs: []int64{0, f0.ID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under general"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under general or f0 or f1 using folder id",
|
||||
folderIDs: []int64{0, f0.ID, f1.ID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under general or f0 using folder UID",
|
||||
folderUIDs: []string{folder.GeneralFolderUID, f0.UID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under general"},
|
||||
},
|
||||
{
|
||||
desc: "find dashboard under general or f0 or f1 using folder UID",
|
||||
folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID},
|
||||
expectedResult: []string{"dashboard under f0", "dashboard under f1", "dashboard under general"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {
|
||||
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
|
||||
dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll},
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: searchstore.TypeDashboard,
|
||||
FolderIds: tc.folderIDs,
|
||||
FolderUIDs: tc.folderUIDs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(tc.expectedResult), len(res))
|
||||
|
||||
for i, r := range tc.expectedResult {
|
||||
assert.Equal(t, r, res[i].Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) {
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
type alertQuery struct {
|
||||
|
||||
@@ -480,6 +480,7 @@ type FindPersistedDashboardsQuery struct {
|
||||
DashboardUIDs []string
|
||||
Type string
|
||||
FolderIds []int64
|
||||
FolderUIDs []string
|
||||
Tags []string
|
||||
Limit int64
|
||||
Page int64
|
||||
|
||||
@@ -38,6 +38,7 @@ type Query struct {
|
||||
DashboardUIDs []string
|
||||
DashboardIds []int64
|
||||
FolderIds []int64
|
||||
FolderUIDs []string
|
||||
Permission dashboards.PermissionType
|
||||
Sort string
|
||||
}
|
||||
@@ -83,6 +84,7 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model.
|
||||
DashboardIds: query.DashboardIds,
|
||||
Type: query.Type,
|
||||
FolderIds: query.FolderIds,
|
||||
FolderUIDs: query.FolderUIDs,
|
||||
Tags: query.Tags,
|
||||
Limit: query.Limit,
|
||||
Page: query.Page,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
@@ -91,6 +92,52 @@ func (f FolderFilter) Where() (string, []interface{}) {
|
||||
return sqlIDin("dashboard.folder_id", f.IDs)
|
||||
}
|
||||
|
||||
type FolderUIDFilter struct {
|
||||
Dialect migrator.Dialect
|
||||
OrgID int64
|
||||
UIDs []string
|
||||
}
|
||||
|
||||
func (f FolderUIDFilter) Where() (string, []interface{}) {
|
||||
if len(f.UIDs) < 1 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
params := []interface{}{}
|
||||
includeGeneral := false
|
||||
for _, uid := range f.UIDs {
|
||||
if uid == folder.GeneralFolderUID {
|
||||
includeGeneral = true
|
||||
continue
|
||||
}
|
||||
params = append(params, uid)
|
||||
}
|
||||
|
||||
q := ""
|
||||
switch {
|
||||
case len(params) < 1:
|
||||
// do nothing
|
||||
case len(params) == 1:
|
||||
q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)"
|
||||
params = append([]interface{}{f.OrgID}, params...)
|
||||
default:
|
||||
sqlArray := "(?" + strings.Repeat(",?", len(params)-1) + ")"
|
||||
q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN " + sqlArray + ")"
|
||||
params = append([]interface{}{f.OrgID}, params...)
|
||||
}
|
||||
|
||||
if includeGeneral {
|
||||
if q == "" {
|
||||
q = "dashboard.folder_id = ? "
|
||||
} else {
|
||||
q = "(" + q + " OR dashboard.folder_id = ?)"
|
||||
}
|
||||
params = append(params, 0)
|
||||
}
|
||||
|
||||
return q, params
|
||||
}
|
||||
|
||||
type DashboardIDFilter struct {
|
||||
IDs []int64
|
||||
}
|
||||
|
||||
59
pkg/services/sqlstore/searchstore/filters_test.go
Normal file
59
pkg/services/sqlstore/searchstore/filters_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package searchstore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFolderUIDFilter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
uids []string
|
||||
expectedSql string
|
||||
expectedParams []interface{}
|
||||
}{
|
||||
{
|
||||
description: "searching general folder",
|
||||
uids: []string{"general"},
|
||||
expectedSql: "dashboard.folder_id = ? ",
|
||||
expectedParams: []interface{}{0},
|
||||
},
|
||||
{
|
||||
description: "searching a specific folder",
|
||||
uids: []string{"abc-123"},
|
||||
expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)",
|
||||
expectedParams: []interface{}{int64(1), "abc-123"},
|
||||
},
|
||||
{
|
||||
description: "searching a specific folders",
|
||||
uids: []string{"abc-123", "def-456"},
|
||||
expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?))",
|
||||
expectedParams: []interface{}{int64(1), "abc-123", "def-456"},
|
||||
},
|
||||
{
|
||||
description: "searching a specific folders or general",
|
||||
uids: []string{"general", "abc-123", "def-456"},
|
||||
expectedSql: "(dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?)) OR dashboard.folder_id = ?)",
|
||||
expectedParams: []interface{}{int64(1), "abc-123", "def-456", 0},
|
||||
},
|
||||
}
|
||||
|
||||
store := setupTestEnvironment(t)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
f := searchstore.FolderUIDFilter{
|
||||
Dialect: store.GetDialect(),
|
||||
OrgID: 1,
|
||||
UIDs: tc.uids,
|
||||
}
|
||||
|
||||
sql, params := f.Where()
|
||||
|
||||
assert.Equal(t, tc.expectedSql, sql)
|
||||
assert.Equal(t, tc.expectedParams, params)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user