mirror of https://github.com/grafana/grafana.git
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:
parent
056b5a7b08
commit
3228ae727e
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue