[search] fix folder sync (#99913)

[search] fix folder sync
This commit is contained in:
Scott Lepper
2025-02-03 15:55:22 -05:00
committed by GitHub
parent 29fa6dfc8d
commit 9677f4b692
2 changed files with 174 additions and 10 deletions
@@ -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[] = [
{
+36 -9
View File
@@ -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);