diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 7f866943cbe..fc170a36783 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "slices" + "sort" "strconv" "strings" @@ -198,6 +199,7 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) { const rootFolder = "general" +//nolint:gocyclo func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -346,6 +348,14 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } + if parsedResults != nil && len(searchRequest.SortBy) == 0 { + // default sort by resource descending ( folders then dashboards ) then title + sort.Slice(parsedResults.Hits, func(i, j int) bool { + return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource || + (parsedResults.Hits[i].Resource == parsedResults.Hits[j].Resource && strings.ToLower(parsedResults.Hits[i].Title) < strings.ToLower(parsedResults.Hits[j].Title)) + }) + } + s.write(w, parsedResults) } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index fbd3a209ff5..83e46a6e6c2 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -2,15 +2,19 @@ package dashboard import ( "context" + "encoding/json" "fmt" "net/http/httptest" "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/grpc" ) @@ -177,7 +181,7 @@ func TestSearchFallback(t *testing.T) { } */ -func TestSearchHandlerFields(t *testing.T) { +func TestSearchHandler(t *testing.T) { // Create a mock client mockClient := &MockClient{} @@ -238,6 +242,33 @@ func TestSearchHandlerFields(t *testing.T) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } }) + + t.Run("Sort - default sort by resource then title", func(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, len(mockResults), len(p.Hits)) + assert.Equal(t, mockResults[3].Value, p.Hits[0].Title) + assert.Equal(t, mockResults[1].Value, p.Hits[3].Title) + }) } // MockClient implements the ResourceIndexClient interface for testing @@ -248,10 +279,59 @@ type MockClient struct { LastSearchRequest *resource.ResourceSearchRequest } +type MockResult struct { + Name string + Resource string + Value string +} + +var mockResults = []MockResult{ + { + Name: "d1", + Resource: "dashboard", + Value: "Dashboard 1", + }, + { + Name: "d2", + Resource: "dashboard", + Value: "Dashboard 2", + }, + { + Name: "f2", + Resource: "folder", + Value: "Folder 2", + }, + { + Name: "f1", + Resource: "folder", + Value: "Folder 1", + }, +} + func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { m.LastSearchRequest = in - return &resource.ResourceSearchResponse{}, nil + rows := make([]*resource.ResourceTableRow, len(mockResults)) + for i, r := range mockResults { + rows[i] = &resource.ResourceTableRow{ + Key: &resource.ResourceKey{ + Name: r.Name, + Resource: r.Resource, + }, + Cells: [][]byte{ + []byte(r.Value), + }, + } + } + + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + {Name: resource.SEARCH_FIELD_TITLE}, + }, + Rows: rows, + }, + }, nil } func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {