diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index 9a9549bfc92..20935ae01b7 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -229,6 +229,13 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso return nil, fmt.Errorf("only one repo name is supported") } query.ManagerIdentity = vals[0] + case resource.SEARCH_FIELD_TITLE_PHRASE: + if len(vals) != 1 { + return nil, fmt.Errorf("only one title supported") + } + + query.Title = vals[0] + query.TitleExactMatch = true } } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index da6b58e2f84..7a6a8779250 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -309,6 +309,38 @@ func TestDashboardSearchClient_Search(t *testing.T) { } }) + t.Run("When searching for SEARCH_FIELD_TITLE_PHRASE, value should be set as title, and ExactMatch should be enabled", func(t *testing.T) { + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + Title: "test", + TitleExactMatch: true, + SignedInUser: user, // user from context should be used + Type: "dash-db", // should set type based off of key + }).Return([]dashboards.DashboardSearchProjection{ + {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + + req := &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + { + Key: resource.SEARCH_FIELD_TITLE_PHRASE, // nolint:staticcheck + Operator: string(selection.Equals), + Values: []string{"test"}, + }, + }, + }, + } + resp, err := client.Search(ctx, req) + + require.NoError(t, err) + require.NotNil(t, resp) + mockStore.AssertExpectations(t) + for _, row := range resp.Results.Rows { + require.Equal(t, len(row.Cells), len(resp.Results.Columns)) + } + }) + t.Run("Should read labels for the dashboard ids", func(t *testing.T) { mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ DashboardIds: []int64{1, 2}, diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 633e8610033..47f80c962e6 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -882,7 +882,7 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F } if len(query.Title) > 0 { - filters = append(filters, searchstore.TitleFilter{Dialect: d.store.GetDialect(), Title: query.Title}) + filters = append(filters, searchstore.TitleFilter{Dialect: d.store.GetDialect(), Title: query.Title, TitleExactMatch: query.TitleExactMatch}) } if len(query.Type) > 0 { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 547737c76fe..72c7af790ed 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -888,25 +888,47 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { } testCases := []struct { - desc string - title string - expectedResult res - typ string + desc string + title string + titleExactMatch bool + expectedResult *res + typ string }{ { desc: "find dashboard under general", title: "dashboard under general", - expectedResult: res{title: "dashboard under general"}, + expectedResult: &res{title: "dashboard under general"}, }, { desc: "find dashboard under f0", title: "dashboard under f0", - expectedResult: res{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, + expectedResult: &res{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}, }, { desc: "find dashboard under subfolder", title: "dashboard under subfolder", - expectedResult: res{title: "dashboard under subfolder", folderUID: subfolder.UID, folderTitle: subfolder.Title}, + expectedResult: &res{title: "dashboard under subfolder", folderUID: subfolder.UID, folderTitle: subfolder.Title}, + }, + { + desc: "find folder 'sub' using partial match: 1 result found", + title: "sub", + titleExactMatch: false, + typ: "dash-folder", + expectedResult: &res{title: "subfolder", folderUID: f0.UID, folderTitle: f0.Title}, + }, + { + desc: "find folder 'sub' using exact match: no results", + title: "sub", + titleExactMatch: true, + typ: "dash-folder", + expectedResult: nil, + }, + { + desc: "find folder 'subfolder' using exact match: 1 result", + title: "subfolder", + titleExactMatch: true, + typ: "dash-folder", + expectedResult: &res{title: "subfolder", folderUID: f0.UID, folderTitle: f0.Title}, }, } @@ -915,11 +937,18 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) require.NoError(t, err) res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{ - SignedInUser: user, - Type: tc.typ, - Title: tc.title, + SignedInUser: user, + Type: tc.typ, + Title: tc.title, + TitleExactMatch: tc.titleExactMatch, }) require.NoError(t, err) + + if tc.expectedResult == nil { + require.Equal(t, 0, len(res)) + return + } + require.Equal(t, 1, len(res)) r := tc.expectedResult diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 0dc8a4e3e8b..edfe75ede75 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -430,12 +430,13 @@ type DashboardACLInfoDTO struct { } type FindPersistedDashboardsQuery struct { - Title string - OrgId int64 - SignedInUser identity.Requester - DashboardIds []int64 - DashboardUIDs []string - Type string + Title string + TitleExactMatch bool + OrgId int64 + SignedInUser identity.Requester + DashboardIds []int64 + DashboardUIDs []string + Type string // Deprecated: use FolderUIDs instead FolderIds []int64 FolderUIDs []string diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 8755d0d585a..72c3d0a323d 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -310,11 +310,16 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 request := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ - Key: folderkey, - Fields: []*resourcepb.Requirement{}, + Key: folderkey, + Fields: []*resourcepb.Requirement{ + { + Key: resource.SEARCH_FIELD_TITLE_PHRASE, // nolint:staticcheck + Operator: string(selection.Equals), + Values: []string{title}, + }, + }, Labels: []*resourcepb.Requirement{}, }, - Query: title, Limit: folderSearchLimit} if parentUID != nil { diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 94532768916..fb26e201889 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -792,11 +792,16 @@ func TestGetFoldersFromApiServer(t *testing.T) { service.unifiedStore = fakeFolderStore fakeK8sClient.On("Search", mock.Anything, int64(1), &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ - Key: folderkey, - Fields: []*resourcepb.Requirement{}, + Key: folderkey, + Fields: []*resourcepb.Requirement{ + &resourcepb.Requirement{ + Key: resource.SEARCH_FIELD_TITLE_PHRASE, // nolint:staticcheck + Operator: string(selection.Equals), + Values: []string{"foo title"}, + }, + }, Labels: []*resourcepb.Requirement{}, }, - Query: "foo title", Limit: folderSearchLimit}). Return(&resourcepb.ResourceSearchResponse{ Results: &resourcepb.ResourceTable{ diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index a5fa3ec84cd..f5487ea4bf2 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -43,11 +43,16 @@ func (f OrgFilter) Where() (string, []any) { } type TitleFilter struct { - Dialect migrator.Dialect - Title string + Dialect migrator.Dialect + Title string + TitleExactMatch bool } func (f TitleFilter) Where() (string, []any) { + if f.TitleExactMatch { + return "dashboard.title = ?", []any{f.Title} + } + sql, params := f.Dialect.LikeOperator("dashboard.title", true, f.Title, true) return sql, []any{params} } diff --git a/pkg/services/sqlstore/searchstore/filters_test.go b/pkg/services/sqlstore/searchstore/filters_test.go index 6cd36e1ba09..186eee1db7d 100644 --- a/pkg/services/sqlstore/searchstore/filters_test.go +++ b/pkg/services/sqlstore/searchstore/filters_test.go @@ -91,3 +91,44 @@ func TestFolderUIDFilter(t *testing.T) { }) } } + +func TestTitleFilter(t *testing.T) { + testCases := []struct { + description string + title string + exactMatch bool + expectedSql string + expectedParams []any + }{ + { + description: "searching foo folder - partial match", + title: "foo", + expectedSql: "dashboard.title LIKE ?", + expectedParams: []any{"%foo%"}, + }, + { + description: "searching foo folder - exact match", + title: "foo", + exactMatch: true, + expectedSql: "dashboard.title = ?", + expectedParams: []any{"foo"}, + }, + } + + store := setupTestEnvironment(t) + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + f := searchstore.TitleFilter{ + Dialect: store.GetDialect(), + Title: tc.title, + TitleExactMatch: tc.exactMatch, + } + + sql, params := f.Where() + + assert.Equal(t, tc.expectedSql, sql) + assert.Equal(t, tc.expectedParams, params) + }) + } +}