From 571b3226ba43d21938a7e23ac248a9a8ca8e0bb8 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 16 Sep 2025 18:17:22 +0300 Subject: [PATCH] Dashboard search: Return description in search results (#110857) * DashList: Add description * Support unified storage * Support unified storage[2] * Exclude description from field * Cleanup * add description * Revert dashlist changes * Update cue * Fix test --------- Co-authored-by: Ryan McKinley --- .../pkg/apis/dashboard/v0alpha1/search.go | 8 +++++--- .../v0alpha1/zz_generated.openapi.go | 9 ++++++++- pkg/registry/apis/dashboard/search.go | 2 +- pkg/registry/apis/dashboard/search_test.go | 6 +++--- .../dashboards/database/database_test.go | 1 + pkg/services/dashboards/models.go | 15 ++++++++------- .../dashboards/service/dashboard_service.go | 2 ++ .../dashboards/service/search/search.go | 19 ++++++++++++++----- .../dashboards/service/search/search_test.go | 9 ++++++++- .../folderimpl/folder_unifiedstorage.go | 17 +++++++++-------- pkg/services/search/model/model.go | 1 + public/api-enterprise-spec.json | 3 +++ public/api-merged.json | 3 +++ public/openapi3.json | 3 +++ 14 files changed, 69 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/search.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/search.go index 5d768dbfc11..ea0084efe2a 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/search.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/search.go @@ -61,18 +61,20 @@ type DashboardHit struct { Resource string `json:"resource"` // dashboards | folders // The k8s "name" (eg, grafana UID) Name string `json:"name"` - // The display nam + // The display name Title string `json:"title"` + // Dashboard description + Description string `json:"description,omitempty"` // Filter tags Tags []string `json:"tags,omitempty"` // The k8s name (eg, grafana UID) for the parent folder Folder string `json:"folder,omitempty"` // Stick untyped extra fields in this object (including the sort value) - Field *common.Unstructured `json:"field,omitempty"` + Field *common.Unstructured `json:"field,omitzero,omitempty"` // When using "real" search, this is the score Score float64 `json:"score,omitempty"` // Explain the score (if possible) - Explain *common.Unstructured `json:"explain,omitempty"` + Explain *common.Unstructured `json:"explain,omitzero,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index 3b6a60b17e0..a26bc80e5d4 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -302,12 +302,19 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref common.ReferenceCallbac }, "title": { SchemaProps: spec.SchemaProps{ - Description: "The display nam", + Description: "The display name", Default: "", Type: []string{"string"}, Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Dashboard description", + Type: []string{"string"}, + Format: "", + }, + }, "tags": { SchemaProps: spec.SchemaProps{ Description: "Filter tags", diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index e00cca791d2..355c193ce48 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -246,7 +246,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { Page: int64(page), // for modes 0-2 (legacy) Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false", } - fields := []string{"title", "folder", "tags"} + fields := []string{"title", "folder", "tags", "description"} if queryParams.Has("field") { // add fields to search and exclude duplicates for _, f := range queryParams["field"] { diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 340ad01dc86..0fd42970135 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -213,7 +213,7 @@ func TestSearchHandler(t *testing.T) { if mockClient.LastSearchRequest == nil { t.Fatalf("expected Search to be called, but it was not") } - expectedFields := []string{"title", "folder", "tags", "field1", "field2", "field3"} + expectedFields := []string{"title", "folder", "tags", "description", "field1", "field2", "field3"} if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } @@ -242,7 +242,7 @@ func TestSearchHandler(t *testing.T) { if mockClient.LastSearchRequest == nil { t.Fatalf("expected Search to be called, but it was not") } - expectedFields := []string{"title", "folder", "tags", "field1"} + expectedFields := []string{"title", "folder", "tags", "description", "field1"} if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } @@ -271,7 +271,7 @@ func TestSearchHandler(t *testing.T) { if mockClient.LastSearchRequest == nil { t.Fatalf("expected Search to be called, but it was not") } - expectedFields := []string{"title", "folder", "tags"} + expectedFields := []string{"title", "folder", "tags", "description"} if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 8a385e4b311..d15baaeffb4 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -1310,6 +1310,7 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb FolderUID: item.FolderUID, FolderTitle: item.FolderTitle, Tags: item.Tags, + Description: item.Description, } if item.FolderUID != "" { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index f48fafc6a81..24b414118f3 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -318,13 +318,14 @@ type SaveDashboardDTO struct { } type DashboardSearchProjection struct { - ID int64 `xorm:"id"` - UID string `xorm:"uid"` - OrgID int64 `xorm:"org_id"` - Title string - Slug string - Term string - IsFolder bool + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + OrgID int64 `xorm:"org_id"` + Title string + Slug string + Term string + Description string + IsFolder bool // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` FolderUID string `xorm:"folder_uid"` diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 7d329290b88..ad83e1d8338 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1468,6 +1468,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb OrgID: query.OrgId, Title: hit.Title, Slug: slugify.Slugify(hit.Title), + Description: hit.Description, IsFolder: false, FolderUID: hit.Folder, FolderTitle: folderTitle, @@ -1569,6 +1570,7 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb FolderUID: item.FolderUID, FolderTitle: item.FolderTitle, Tags: []string{}, + Description: item.Description, } if item.Tags != nil { diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go index 18c00ccacef..93fdf30689a 100644 --- a/pkg/services/dashboards/service/search/search.go +++ b/pkg/services/dashboards/service/search/search.go @@ -14,11 +14,12 @@ import ( var ( excludedFields = map[string]string{ - resource.SEARCH_FIELD_EXPLAIN: "", - resource.SEARCH_FIELD_SCORE: "", - resource.SEARCH_FIELD_TITLE: "", - resource.SEARCH_FIELD_FOLDER: "", - resource.SEARCH_FIELD_TAGS: "", + resource.SEARCH_FIELD_EXPLAIN: "", + resource.SEARCH_FIELD_SCORE: "", + resource.SEARCH_FIELD_TITLE: "", + resource.SEARCH_FIELD_FOLDER: "", + resource.SEARCH_FIELD_TAGS: "", + resource.SEARCH_FIELD_DESCRIPTION: "", } IncludeFields = []string{ @@ -26,6 +27,7 @@ var ( resource.SEARCH_FIELD_TAGS, resource.SEARCH_FIELD_LABELS, resource.SEARCH_FIELD_FOLDER, + resource.SEARCH_FIELD_DESCRIPTION, resource.SEARCH_FIELD_CREATED, resource.SEARCH_FIELD_CREATED_BY, resource.SEARCH_FIELD_UPDATED, @@ -38,6 +40,7 @@ var ( } ) +// nolint:gocyclo func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0alpha1.SearchResults, error) { if result == nil { return v0alpha1.SearchResults{}, nil @@ -50,6 +53,7 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al titleIDX := -1 folderIDX := -1 tagsIDX := -1 + descriptionIDX := -1 scoreIDX := -1 explainIDX := -1 @@ -65,6 +69,8 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al folderIDX = i case resource.SEARCH_FIELD_TAGS: tagsIDX = i + case resource.SEARCH_FIELD_DESCRIPTION: + descriptionIDX = i } } @@ -113,6 +119,9 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al if folderIDX >= 0 && row.Cells[folderIDX] != nil { hit.Folder = string(row.Cells[folderIDX]) } + if descriptionIDX >= 0 && row.Cells[descriptionIDX] != nil { + hit.Description = string(row.Cells[descriptionIDX]) + } if tagsIDX >= 0 && row.Cells[tagsIDX] != nil { _ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags) } diff --git a/pkg/services/dashboards/service/search/search_test.go b/pkg/services/dashboards/service/search/search_test.go index 69b76de472e..11279799658 100644 --- a/pkg/services/dashboards/service/search/search_test.go +++ b/pkg/services/dashboards/service/search/search_test.go @@ -32,6 +32,10 @@ func TestParseResults(t *testing.T) { Name: search.DASHBOARD_LINK_COUNT, Type: resourcepb.ResourceTableColumnDefinition_INT32, }, + { + Name: "description", + Type: resourcepb.ResourceTableColumnDefinition_STRING, + }, }, Rows: []*resourcepb.ResourceTableRow{ { @@ -44,6 +48,7 @@ func TestParseResults(t *testing.T) { []byte("folder1"), []byte("100"), []byte("25"), + []byte("description"), }, }, }, @@ -51,8 +56,10 @@ func TestParseResults(t *testing.T) { TotalHits: 1, } - _, err := ParseResults(resSearchResp, 0) + results, err := ParseResults(resSearchResp, 0) require.NoError(t, err) + require.Len(t, results.Hits, 1) + require.Equal(t, "description", results.Hits[0].Description) }) t.Run("should return error when trying to parse results with mismatch length between Columns and row Cells", func(t *testing.T) { diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index f9f7af3b8ea..7031ea70f20 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -224,14 +224,15 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S for i, item := range parsedResults.Hits { slug := slugify.Slugify(item.Title) hitList[i] = &model.Hit{ - ID: item.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), - UID: item.Name, - OrgID: query.OrgID, - Title: item.Title, - URI: "db/" + slug, - URL: dashboards.GetFolderURL(item.Name, slug), - Type: model.DashHitFolder, - FolderUID: item.Folder, + ID: item.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), + UID: item.Name, + OrgID: query.OrgID, + Title: item.Title, + URI: "db/" + slug, + URL: dashboards.GetFolderURL(item.Name, slug), + Type: model.DashHitFolder, + FolderUID: item.Folder, + Description: item.Description, } } diff --git a/pkg/services/search/model/model.go b/pkg/services/search/model/model.go index afad2f0ff5d..970bc82ee5c 100644 --- a/pkg/services/search/model/model.go +++ b/pkg/services/search/model/model.go @@ -73,6 +73,7 @@ type Hit struct { Type HitType `json:"type"` Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` + Description string `json:"description,omitempty"` FolderID int64 `json:"folderId,omitempty"` // Deprecated: use FolderUID instead FolderUID string `json:"folderUid,omitempty"` FolderTitle string `json:"folderTitle,omitempty"` diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index ea2046dedd7..0f25b53a11a 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5138,6 +5138,9 @@ "Hit": { "type": "object", "properties": { + "description": { + "type": "string" + }, "folderId": { "type": "integer", "format": "int64" diff --git a/public/api-merged.json b/public/api-merged.json index 830f4f4c309..ef550058eec 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17024,6 +17024,9 @@ "Hit": { "type": "object", "properties": { + "description": { + "type": "string" + }, "folderId": { "type": "integer", "format": "int64" diff --git a/public/openapi3.json b/public/openapi3.json index 9140c87244d..82afc3051fc 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6551,6 +6551,9 @@ }, "Hit": { "properties": { + "description": { + "type": "string" + }, "folderId": { "format": "int64", "type": "integer"