wires up dashboards page to be able to sort by usage stats (sprinkles) (#99479)

* wires up dashboards page to be able to sort by usage stats (sprinkles)

* dont mutate field

* use better type for field

* adds tests. Had to export some types and put the field type back to object.

* frontend asks for sort field in response if needed

* adds some unit tests for getSortOptions

* use Record instead of object

* prettier

* adds ternaries, another unit test
This commit is contained in:
owensmallwood 2025-01-28 11:36:26 -06:00 committed by GitHub
parent 056b5a7b08
commit 3228ae727e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 120 additions and 5 deletions

View File

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

View File

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

View File

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

View File

@ -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<string, string | number>; // 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<SearchAPIResponse>(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<SearchAPIResponse>(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 });
}