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 <ryantxu@gmail.com>
This commit is contained in:
Alex Khomenko 2025-09-16 18:17:22 +03:00 committed by GitHub
parent fad8891b1a
commit 571b3226ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 69 additions and 29 deletions

View File

@ -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

View File

@ -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",

View File

@ -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"] {

View File

@ -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)
}

View File

@ -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 != "" {

View File

@ -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"`

View File

@ -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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,
}
}

View File

@ -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"`

View File

@ -5138,6 +5138,9 @@
"Hit": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"folderId": {
"type": "integer",
"format": "int64"

View File

@ -17024,6 +17024,9 @@
"Hit": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"folderId": {
"type": "integer",
"format": "int64"

View File

@ -6551,6 +6551,9 @@
},
"Hit": {
"properties": {
"description": {
"type": "string"
},
"folderId": {
"format": "int64",
"type": "integer"