diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 79a5149efc7..d0c8b0f4ca7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -655,6 +655,10 @@ func getSortFields(req *resource.ResourceSearchRequest) []string { input = field } + if slices.Contains(DashboardFields(), input) { + input = "fields." + input + } + if sort.Desc { input = "-" + input } diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index effc77bdd57..4535b276c0a 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -529,6 +529,36 @@ func TestBleveBackend(t *testing.T) { }) } +func TestGetSortFields(t *testing.T) { + t.Run("will prepend 'fields.' to sort fields when they are dashboard fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"fields.views_total"}, sortFields) + }) + t.Run("will prepend sort fields with a '-' when sort is Desc", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "views_total", Desc: true}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"-fields.views_total"}, sortFields) + }) + t.Run("will not prepend 'fields.' to common fields", func(t *testing.T) { + searchReq := &resource.ResourceSearchRequest{ + SortBy: []*resource.ResourceSearchRequest_Sort{ + {Field: "description", Desc: false}, + }, + } + sortFields := getSortFields(searchReq) + assert.Equal(t, []string{"description"}, sortFields) + }) +} + func asTimePointer(milli int64) *time.Time { if milli > 0 { t := time.UnixMilli(milli) diff --git a/public/app/features/search/service/unified.test.ts b/public/app/features/search/service/unified.test.ts new file mode 100644 index 00000000000..4d50e25193b --- /dev/null +++ b/public/app/features/search/service/unified.test.ts @@ -0,0 +1,65 @@ +import { toDashboardResults, SearchHit, SearchAPIResponse } from './unified'; + +describe('Unified Storage Searcher', () => { + it('can create dashboard search results and set meta sortBy so column is added for sprinkles sort field', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 2 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 2, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, 'errors_today'); + + expect(results.length).toBe(2); + const sprinklesField = results.fields[10]; + expect(sprinklesField.name).toBe('errors_today'); + expect(sprinklesField.values).toEqual([1, 2]); // this also tests the hits original order is preserved + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); + + it('will trim "-" from the sort field name', () => { + const mockHits: SearchHit[] = [ + { + resource: 'dashboard', + name: 'Main Dashboard', + title: 'Main Dashboard Title', + location: '/dashboards/1', + folder: 'General', + tags: ['monitoring', 'performance'], + field: { errors_today: 1 }, + url: '/dashboards/1', + }, + ]; + + const mockResponse: SearchAPIResponse = { + totalHits: 0, + hits: mockHits, + facets: {}, + }; + const results = toDashboardResults(mockResponse, '-errors_today'); + + expect(results.meta?.custom?.sortBy).toBe('errors_today'); + }); +}); diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 9e379873cc3..3ed44146c35 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -20,7 +20,7 @@ const loadingFrameName = 'Loading'; const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${config.namespace}/search`; -type SearchHit = { +export type SearchHit = { resource: string; // dashboards | folders name: string; title: string; @@ -28,11 +28,13 @@ type SearchHit = { folder: string; tags: string[]; + field: Record; // extra fields from the backend - sort fields included here as well + // calculated in the frontend url: string; }; -type SearchAPIResponse = { +export type SearchAPIResponse = { totalHits: number; hits: SearchHit[]; facets?: { @@ -110,7 +112,7 @@ export class UnifiedSearcher implements GrafanaSearcher { const uri = await this.newRequest(query); const rsp = await getBackendSrv().get(uri); - const first = toDashboardResults(rsp); + const first = toDashboardResults(rsp, query.sort ?? ''); if (first.name === loadingFrameName) { return this.fallbackSearcher.search(query); } @@ -145,7 +147,7 @@ export class UnifiedSearcher implements GrafanaSearcher { } const nextPageUrl = `${uri}&offset=${offset}`; const resp = await getBackendSrv().get(nextPageUrl); - const frame = toDashboardResults(resp); + const frame = toDashboardResults(resp, query.sort ?? ''); if (!frame) { console.log('no results', frame); return; @@ -217,6 +219,9 @@ export class UnifiedSearcher implements GrafanaSearcher { if (query.sort) { const sort = query.sort.replace('_sort', '').replace('name', 'title'); uri += `&sort=${sort}`; + const sortField = sort.startsWith('-') ? sort.substring(1) : sort; + + uri += `&field=${sortField}`; // we want to the sort field to be included in the response } if (query.name?.length) { @@ -279,7 +284,7 @@ function getSortFieldDisplayName(name: string) { return name; } -function toDashboardResults(rsp: SearchAPIResponse): DataFrame { +export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFrame { const hits = rsp.hits; if (hits.length < 1) { return { fields: [], length: 0 }; @@ -290,6 +295,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location = 'general'; } + // display null field values as "-" + const field = Object.fromEntries( + Object.entries(hit.field ?? {}).map(([key, value]) => [key, value == null ? '-' : value]) + ); + return { ...hit, uid: hit.name, @@ -299,6 +309,7 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { location, name: hit.title, // 🤯 FIXME hit.name is k8s name, eg grafana dashboards UID kind: hit.resource.substring(0, hit.resource.length - 1), // dashboard "kind" is not plural + ...field, }; }); const frame = toDataFrame(dashboardHits); @@ -308,6 +319,11 @@ function toDashboardResults(rsp: SearchAPIResponse): DataFrame { max_score: 1, }, }; + if (sort && frame.meta.custom) { + // trim the "-" from sort if it exists + frame.meta.custom.sortBy = sort.startsWith('-') ? sort.substring(1) : sort; + } + for (const field of frame.fields) { field.display = getDisplayProcessor({ field, theme: config.theme2 }); }