@@ -1,6 +1,143 @@
|
||||
import { toDashboardResults, SearchHit, SearchAPIResponse } from './unified';
|
||||
import { BackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { GrafanaSearcher, SearchQuery } from './types';
|
||||
import { toDashboardResults, SearchHit, SearchAPIResponse, UnifiedSearcher } from './unified';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockResults: SearchAPIResponse = {
|
||||
hits: [],
|
||||
totalHits: 0,
|
||||
};
|
||||
|
||||
const mockFolders: SearchAPIResponse = {
|
||||
hits: [],
|
||||
totalHits: 0,
|
||||
};
|
||||
|
||||
const mockFallbackSearcher = {
|
||||
search: jest.fn(),
|
||||
} as unknown as GrafanaSearcher;
|
||||
|
||||
const getResponse = (uri: string) => {
|
||||
if (uri.endsWith('?type=folders')) {
|
||||
return Promise.resolve(mockFolders);
|
||||
}
|
||||
return Promise.resolve(mockResults);
|
||||
};
|
||||
|
||||
const mockSearcher = {
|
||||
search: (uri: string) => getResponse(uri),
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const originalRuntime = jest.requireActual('@grafana/runtime');
|
||||
return {
|
||||
...originalRuntime,
|
||||
getBackendSrv: () =>
|
||||
({
|
||||
get: (uri: string) => mockSearcher.search(uri),
|
||||
}) as unknown as BackendSrv,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Unified Storage Searcher', () => {
|
||||
it('should perform search with basic query', async () => {
|
||||
mockFolders.hits = [
|
||||
{
|
||||
name: 'folder1',
|
||||
title: 'Folder 1',
|
||||
resource: 'folders',
|
||||
} as SearchHit,
|
||||
];
|
||||
mockResults.hits = [
|
||||
{
|
||||
name: 'dashboard1',
|
||||
title: 'Dashboard 1',
|
||||
resource: 'dashboards',
|
||||
folder: 'folder1',
|
||||
} as SearchHit,
|
||||
];
|
||||
|
||||
const query: SearchQuery = {
|
||||
query: 'test',
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
const searcher = new UnifiedSearcher(mockFallbackSearcher);
|
||||
|
||||
const response = await searcher.search(query);
|
||||
|
||||
expect(response.view.length).toBe(1);
|
||||
expect(response.view.get(0).title).toBe('Dashboard 1');
|
||||
|
||||
const df = response.view.dataFrame;
|
||||
const locationInfo = df.meta?.custom?.locationInfo;
|
||||
expect(locationInfo).toBeDefined();
|
||||
expect(locationInfo?.folder1.name).toBe('Folder 1');
|
||||
});
|
||||
|
||||
it('should perform search and sync folders with missing folder', async () => {
|
||||
const mockFolders = {
|
||||
hits: [
|
||||
{
|
||||
name: 'folder2',
|
||||
title: 'Folder 2',
|
||||
resource: 'folders',
|
||||
} as SearchHit,
|
||||
],
|
||||
totalHits: 1,
|
||||
};
|
||||
|
||||
const mockResults = {
|
||||
hits: [
|
||||
{
|
||||
name: 'db1',
|
||||
title: 'DB 1',
|
||||
resource: 'dashboards',
|
||||
folder: 'folder1',
|
||||
} as SearchHit,
|
||||
{
|
||||
name: 'db2',
|
||||
title: 'DB 2',
|
||||
resource: 'dashboards',
|
||||
folder: 'folder2',
|
||||
} as SearchHit,
|
||||
],
|
||||
totalHits: 2,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(mockSearcher, 'search')
|
||||
.mockResolvedValueOnce(mockFolders)
|
||||
.mockResolvedValueOnce(mockResults)
|
||||
.mockResolvedValueOnce(mockFolders);
|
||||
|
||||
const consoleWarn = jest.fn();
|
||||
jest.spyOn(console, 'warn').mockImplementationOnce(consoleWarn);
|
||||
|
||||
const query: SearchQuery = {
|
||||
query: 'test',
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
const searcher = new UnifiedSearcher(mockFallbackSearcher);
|
||||
|
||||
const response = await searcher.search(query);
|
||||
|
||||
expect(response.view.length).toBe(1);
|
||||
expect(response.view.get(0).title).toBe('DB 2');
|
||||
|
||||
const df = response.view.dataFrame;
|
||||
const locationInfo = df.meta?.custom?.locationInfo;
|
||||
expect(locationInfo).toBeDefined();
|
||||
expect(locationInfo?.folder2.name).toBe('Folder 2');
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
expect(mockSearcher.search).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('can create dashboard search results and set meta sortBy so column is added for sprinkles sort field', () => {
|
||||
const mockHits: SearchHit[] = [
|
||||
{
|
||||
|
||||
@@ -112,7 +112,7 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
|
||||
async doSearchQuery(query: SearchQuery): Promise<QueryResponse> {
|
||||
const uri = await this.newRequest(query);
|
||||
const rsp = await getBackendSrv().get<SearchAPIResponse>(uri);
|
||||
const rsp = await this.fetchResponse(uri);
|
||||
|
||||
const first = toDashboardResults(rsp, query.sort ?? '');
|
||||
if (first.name === loadingFrameName) {
|
||||
@@ -120,12 +120,6 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
}
|
||||
|
||||
const meta = first.meta?.custom || ({} as SearchResultMeta);
|
||||
const locationInfo = await this.locationInfo;
|
||||
const hasMissing = rsp.hits.some((hit) => !locationInfo[hit.folder]);
|
||||
if (hasMissing) {
|
||||
// sync the location info ( folders )
|
||||
this.locationInfo = loadLocationInfo();
|
||||
}
|
||||
meta.locationInfo = await this.locationInfo;
|
||||
|
||||
// Set the field name to a better display name
|
||||
@@ -141,14 +135,13 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
let loadMax = 0;
|
||||
let pending: Promise<void> | undefined = undefined;
|
||||
const getNextPage = async () => {
|
||||
// TODO: implement this correctly
|
||||
while (loadMax > view.dataFrame.length) {
|
||||
const offset = view.dataFrame.length;
|
||||
if (offset >= meta.count) {
|
||||
return;
|
||||
}
|
||||
const nextPageUrl = `${uri}&offset=${offset}`;
|
||||
const resp = await getBackendSrv().get<SearchAPIResponse>(nextPageUrl);
|
||||
const resp = await this.fetchResponse(nextPageUrl);
|
||||
const frame = toDashboardResults(resp, query.sort ?? '');
|
||||
if (!frame) {
|
||||
console.log('no results', frame);
|
||||
@@ -198,6 +191,40 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
};
|
||||
}
|
||||
|
||||
async fetchResponse(uri: string) {
|
||||
const rsp = await getBackendSrv().get<SearchAPIResponse>(uri);
|
||||
const isFolderCacheStale = await this.isFolderCacheStale(rsp.hits);
|
||||
if (!isFolderCacheStale) {
|
||||
return rsp;
|
||||
}
|
||||
// sync the location info ( folders )
|
||||
this.locationInfo = loadLocationInfo();
|
||||
// recheck for missing folders
|
||||
const hasMissing = await this.isFolderCacheStale(rsp.hits);
|
||||
if (!hasMissing) {
|
||||
return rsp;
|
||||
}
|
||||
// we still have results here with folders we can't find
|
||||
// filter the results since we probably don't have access to that folder
|
||||
const locationInfo = await this.locationInfo;
|
||||
const hits = rsp.hits.filter((hit) => {
|
||||
if (hit.folder === undefined || locationInfo[hit.folder] !== undefined) {
|
||||
return true;
|
||||
}
|
||||
console.warn('Dropping search hit with missing folder', hit);
|
||||
return false;
|
||||
});
|
||||
const totalHits = rsp.totalHits - (rsp.hits.length - hits.length);
|
||||
return { ...rsp, hits, totalHits };
|
||||
}
|
||||
|
||||
async isFolderCacheStale(hits: SearchHit[]): Promise<boolean> {
|
||||
const locationInfo = await this.locationInfo;
|
||||
return hits.some((hit) => {
|
||||
return hit.folder !== undefined && locationInfo[hit.folder] === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private async newRequest(query: SearchQuery): Promise<string> {
|
||||
query = await replaceCurrentFolderQuery(query);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user