From 0c38a3ba9700bc16954bcaa6e3f862cde4a79782 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 20 Sep 2022 17:24:29 -0700 Subject: [PATCH] Search: Investigate frontend search options (again) (#55526) --- .../app/features/search/service/frontend.ts | 104 ++++++++++++++++++ .../app/features/search/service/searcher.ts | 7 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 public/app/features/search/service/frontend.ts diff --git a/public/app/features/search/service/frontend.ts b/public/app/features/search/service/frontend.ts new file mode 100644 index 00000000000..55a703c0229 --- /dev/null +++ b/public/app/features/search/service/frontend.ts @@ -0,0 +1,104 @@ +import { DataFrameView, SelectableValue, ArrayVector } from '@grafana/data'; +import { TermCount } from 'app/core/components/TagFilter/TagFilter'; + +import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery } from '.'; + +export class FrontendSearcher implements GrafanaSearcher { + readonly cache = new Map(); + + constructor(private parent: GrafanaSearcher) {} + + async search(query: SearchQuery): Promise { + if (query.facet?.length) { + throw new Error('facets not supported!'); + } + // Don't bother... not needed for this exercise + if (query.tags?.length || query.ds_uid?.length) { + return this.parent.search(query); + } + + // TODO -- make sure we refresh after a while + const all = await this.getCache(query.kind); + const view = all.search(query.query); + return { + isItemLoaded: () => true, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise => {}, + totalRows: view.length, + view, + }; + } + + async getCache(kind?: string[]): Promise { + const key = kind ? kind.join(',') : '*'; + let res = this.cache.get(key); + if (res) { + return Promise.resolve(res); + } + + const v = await this.parent.search({ + kind, // match the request + limit: 5000, // max for now + }); + + res = new FullResultCache(v.view); + this.cache.set(key, res); + return res; + } + + async starred(query: SearchQuery): Promise { + return this.parent.starred(query); + } + + // returns the appropriate sorting options + async getSortOptions(): Promise { + return this.parent.getSortOptions(); + } + + async tags(query: SearchQuery): Promise { + return this.parent.tags(query); + } +} + +class FullResultCache { + readonly lower: string[]; + empty: DataFrameView; + + constructor(private full: DataFrameView) { + this.lower = this.full.fields.name.values.toArray().map((v) => (v ? v.toLowerCase() : '')); + + // Copy with empty values + this.empty = new DataFrameView({ + ...this.full.dataFrame, // copy folder metadata + fields: this.full.dataFrame.fields.map((v) => ({ ...v, values: new ArrayVector([]) })), + length: 0, // for now + }); + } + + // single instance that is mutated for each response (not great, but OK for now) + search(query?: string): DataFrameView { + if (!query?.length || query === '*') { + return this.full; + } + const match = query.toLowerCase(); + const allFields = this.full.dataFrame.fields; + + // eslint-disable-next-line + const values = allFields.map((v) => [] as any[]); // empty value for each field + + for (let i = 0; i < this.lower.length; i++) { + if (this.lower[i].indexOf(match) >= 0) { + for (let c = 0; c < allFields.length; c++) { + values[c].push(allFields[c].values.get(i)); + } + } + } + + // mutates the search object + this.empty.dataFrame.fields.forEach((f, idx) => { + f.values = new ArrayVector(values[idx]); // or just set it? + }); + this.empty.dataFrame.length = this.empty.dataFrame.fields[0].values.length; + + return this.empty; + } +} diff --git a/public/app/features/search/service/searcher.ts b/public/app/features/search/service/searcher.ts index a43af21d386..fe017d1837f 100644 --- a/public/app/features/search/service/searcher.ts +++ b/public/app/features/search/service/searcher.ts @@ -1,16 +1,21 @@ import { config } from '@grafana/runtime'; import { BlugeSearcher } from './bluge'; +import { FrontendSearcher } from './frontend'; import { SQLSearcher } from './sql'; import { GrafanaSearcher } from './types'; let searcher: GrafanaSearcher | undefined = undefined; export function getGrafanaSearcher(): GrafanaSearcher { - const sqlSearcher = new SQLSearcher(); if (!searcher) { + const sqlSearcher = new SQLSearcher(); const useBluge = config.featureToggles.panelTitleSearch; searcher = useBluge ? new BlugeSearcher(sqlSearcher) : sqlSearcher; + + if (useBluge && location.search.indexOf('do-frontend-query')) { + searcher = new FrontendSearcher(searcher); + } } return searcher!; }