K8s: Search fallback: Support all sort by methods (#100776)

This commit is contained in:
Stephanie Hingtgen
2025-02-18 12:30:11 -06:00
committed by GitHub
parent 7b3b165c3d
commit 67be9aeed6
43 changed files with 356 additions and 99 deletions
@@ -24,6 +24,7 @@ import (
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
@@ -68,8 +69,9 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
softDelete bool,
sorter sort.Service,
) DashboardAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore)
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
return &dashboardSqlAccess{
sql: sql,
namespacer: namespacer,
@@ -14,9 +14,10 @@ import (
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/selection"
)
@@ -24,10 +25,19 @@ import (
type DashboardSearchClient struct {
resource.ResourceIndexClient
dashboardStore dashboards.Store
sorter sort.Service
}
func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient {
return &DashboardSearchClient{dashboardStore: dashboardStore}
func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Service) *DashboardSearchClient {
return &DashboardSearchClient{dashboardStore: dashboardStore, sorter: sorter}
}
var sortByMapping = map[string]string{
unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently-",
unisearch.DASHBOARD_VIEWS_TOTAL: "viewed-",
unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently-",
unisearch.DASHBOARD_ERRORS_TOTAL: "errors-",
"title": "alpha-",
}
// nolint:gocyclo
@@ -78,16 +88,30 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
query.Type = queryType
}
// technically, there exists the ability to register multiple ways of sorting using the legacy database
// see RegisterSortOption in pkg/services/search/sorting.go
// however, it doesn't look like we are taking advantage of that. And since by default the legacy
// sql will sort by title ascending, we only really need to handle the "alpha-desc" case
if req.SortBy != nil {
for _, sort := range req.SortBy {
if sort.Field == "title" && sort.Desc {
query.Sort = search.SortAlphaDesc
}
sortByField := ""
if len(req.SortBy) != 0 {
if len(req.SortBy) > 1 {
return nil, fmt.Errorf("only one sort field is supported")
}
sort := req.SortBy[0]
sortByField = strings.TrimPrefix(sort.Field, resource.SEARCH_FIELD_PREFIX)
sorterName := sortByMapping[sortByField]
if sort.Desc {
sorterName += "desc"
} else {
sorterName += "asc"
}
if sorter, ok := c.sorter.GetSortOption(sorterName); ok {
query.Sort = sorter
}
}
// the title search will not return any sortMeta (an int64), like
// most sorting will. Without this, the title will be set to sortMeta (0)
if sortByField == resource.SEARCH_FIELD_TITLE {
sortByField = ""
}
// if searching for tags, get those instead of the dashboards or folders
@@ -178,7 +202,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
query.ProvisionedRepo, _ = dashboard.GetProvisionedFileNameFromMeta(vals[0])
}
}
searchFields := resource.StandardSearchFields()
list := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
@@ -186,6 +209,10 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
&resource.ResourceTableColumnDefinition{
Name: sortByField,
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
},
}
@@ -213,7 +240,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.UID,
}, req.Options.Key.Namespace),
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}},
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}, {}},
})
}
@@ -235,7 +262,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
Key: getResourceKey(dashboard, req.Options.Key.Namespace),
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags},
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags, []byte(strconv.FormatInt(dashboard.SortMeta, 10))},
})
}
@@ -276,6 +303,7 @@ func formatQueryResult(res []dashboards.DashboardSearchProjection) []*dashboards
FolderUID: item.FolderUID,
Tags: []string{},
IsFolder: item.IsFolder,
SortMeta: item.SortMeta,
}
hitList = append(hitList, hit)
hits[key] = hit
@@ -3,14 +3,18 @@ package legacysearcher
import (
"context"
"encoding/json"
"strconv"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/selection"
@@ -18,10 +22,13 @@ import (
func TestDashboardSearchClient_Search(t *testing.T) {
mockStore := dashboards.NewFakeDashboardStore(t)
client := NewDashboardSearchClient(mockStore)
sortSvc := sort.ProvideService()
client := NewDashboardSearchClient(mockStore, sortSvc)
ctx := context.Background()
user := &user.SignedInUser{OrgID: 2}
ctx = identity.WithRequester(ctx, user)
emptyTags, err := json.Marshal([]string{})
require.NoError(t, err)
dashboardKey := &resource.ResourceKey{
Name: "uid",
@@ -29,9 +36,11 @@ func TestDashboardSearchClient_Search(t *testing.T) {
}
t.Run("Should parse results into GRPC", func(t *testing.T) {
sorter, _ := sortSvc.GetSortOption("alpha-asc")
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
Sort: sorter,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
@@ -41,17 +50,19 @@ func TestDashboardSearchClient_Search(t *testing.T) {
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: resource.SEARCH_FIELD_TITLE,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
tags, err := json.Marshal([]string{"term"})
require.NoError(t, err)
emptyTags, err := json.Marshal([]string{})
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 2,
@@ -60,6 +71,10 @@ func TestDashboardSearchClient_Search(t *testing.T) {
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
&resource.ResourceTableColumnDefinition{
Name: "", // sort by should be empty if title is what we sorted by
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
@@ -72,6 +87,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
[]byte("Test Dashboard"),
[]byte("folder1"),
tags,
[]byte(strconv.FormatInt(0, 10)),
},
},
{
@@ -84,6 +100,127 @@ func TestDashboardSearchClient_Search(t *testing.T) {
[]byte("Test Dashboard2"),
[]byte("folder2"),
emptyTags,
[]byte(strconv.FormatInt(0, 10)),
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Sorting should be properly parsed into legacy sorting options (asc), and results added", func(t *testing.T) {
sortOptionAsc := model.SortOption{
Name: "viewed-asc", // should add -asc to the sort field and match on that
}
sortSvc.RegisterSortOption(sortOptionAsc)
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: "dash-db",
Sort: sortOptionAsc,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50)},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed
Desc: false,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 1,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
&resource.ResourceTableColumnDefinition{
Name: "views_total",
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder"),
emptyTags,
[]byte(strconv.FormatInt(50, 10)),
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Sorting should be properly parsed into legacy sorting options (desc)", func(t *testing.T) {
sortOptionAsc := model.SortOption{
Name: "errors-recently-desc", // should add -asc to the sort field and match on that
}
sortSvc.RegisterSortOption(sortOptionAsc)
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: "dash-db",
Sort: sortOptionAsc,
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2)},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
SortBy: []*resource.ResourceSearchRequest_Sort{
{
Field: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS,
Desc: true,
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 1,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
&resource.ResourceTableColumnDefinition{
Name: "errors_last_30_days",
Type: resource.ResourceTableColumnDefinition_INT64,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder"),
emptyTags,
[]byte(strconv.FormatInt(2, 10)),
},
},
},
+1 -1
View File
@@ -290,7 +290,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
if queryParams.Has("sort") {
for _, sort := range queryParams["sort"] {
if slices.Contains(search.DashboardFields(), sort) {
sort = "fields." + sort
sort = resource.SEARCH_FIELD_PREFIX + sort
}
s := &resource.ResourceSearchRequest_Sort{Field: sort}
if strings.HasPrefix(sort, "-") {
@@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
@@ -68,11 +69,12 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
@@ -86,7 +88,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
legacy: &dashboard.DashboardStorage{
Resource: dashboardv0alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv0alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},
@@ -29,6 +29,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
@@ -81,7 +83,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
legacy: &dashboard.DashboardStorage{
Resource: dashboardv1alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv1alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},
@@ -29,6 +29,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
sql db.DB,
tracing *tracing.TracingService,
unified resource.ResourceClient,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
@@ -82,7 +84,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
legacy: &dashboard.DashboardStorage{
Resource: dashboardv2alpha1.DashboardResourceInfo,
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
TableConverter: dashboardv2alpha1.DashboardResourceInfo.TableConverter(),
Features: features,
},