mirror of https://github.com/grafana/grafana.git
				
				
				
			Grafana/data: Extract fuzzy search core (#107110)
* Move fuzzy search to grafana/data * Move @leeoniya/ufuzzy package * Cleanup * Use exact version * mark export as internal
This commit is contained in:
		
							parent
							
								
									24884154dc
								
							
						
					
					
						commit
						3d1b820827
					
				| 
						 | 
					@ -57,6 +57,7 @@
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@braintree/sanitize-url": "7.0.1",
 | 
					    "@braintree/sanitize-url": "7.0.1",
 | 
				
			||||||
    "@grafana/schema": "12.1.0-pre",
 | 
					    "@grafana/schema": "12.1.0-pre",
 | 
				
			||||||
 | 
					    "@leeoniya/ufuzzy": "1.0.18",
 | 
				
			||||||
    "@types/d3-interpolate": "^3.0.0",
 | 
					    "@types/d3-interpolate": "^3.0.0",
 | 
				
			||||||
    "@types/string-hash": "1.1.3",
 | 
					    "@types/string-hash": "1.1.3",
 | 
				
			||||||
    "@types/systemjs": "6.15.1",
 | 
					    "@types/systemjs": "6.15.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -258,8 +258,9 @@ export * as arrayUtils from './utils/arrayUtils';
 | 
				
			||||||
export { store, Store } from './utils/store';
 | 
					export { store, Store } from './utils/store';
 | 
				
			||||||
export { LocalStorageValueProvider } from './utils/LocalStorageValueProvider';
 | 
					export { LocalStorageValueProvider } from './utils/LocalStorageValueProvider';
 | 
				
			||||||
export { throwIfAngular } from './utils/throwIfAngular';
 | 
					export { throwIfAngular } from './utils/throwIfAngular';
 | 
				
			||||||
 | 
					export { fuzzySearch } from './utils/fuzzySearch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Tranformations
 | 
					// Transformations
 | 
				
			||||||
export { standardTransformers } from './transformations/transformers';
 | 
					export { standardTransformers } from './transformations/transformers';
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
  fieldMatchers,
 | 
					  fieldMatchers,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,103 @@
 | 
				
			||||||
 | 
					import { fuzzySearch } from './fuzzySearch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('fuzzySearch', () => {
 | 
				
			||||||
 | 
					  it('should return all indices when needle is empty', () => {
 | 
				
			||||||
 | 
					    const haystack = ['A', 'B', 'C', 'D'];
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, '');
 | 
				
			||||||
 | 
					    expect(result).toEqual([0, 1, 2, 3]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should properly rank by match quality', () => {
 | 
				
			||||||
 | 
					    const haystack = ['A', 'AA', 'AB', 'AC', 'BC', 'C', 'CD'];
 | 
				
			||||||
 | 
					    const needle = 'C';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const matches = result.map((idx) => haystack[idx]);
 | 
				
			||||||
 | 
					    expect(matches).toEqual(['C', 'CD', 'AC', 'BC']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should handle case sensitivity and order by match quality', () => {
 | 
				
			||||||
 | 
					    const haystack = [
 | 
				
			||||||
 | 
					      'client_service_namespace',
 | 
				
			||||||
 | 
					      'namespace',
 | 
				
			||||||
 | 
					      'alert_namespace',
 | 
				
			||||||
 | 
					      'container_namespace',
 | 
				
			||||||
 | 
					      'Namespace',
 | 
				
			||||||
 | 
					      'client_k8s_namespace_name',
 | 
				
			||||||
 | 
					      'foobar',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    const needle = 'Names';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					    const matches = result.map((idx) => haystack[idx]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(matches).toEqual([
 | 
				
			||||||
 | 
					      'Namespace',
 | 
				
			||||||
 | 
					      'namespace',
 | 
				
			||||||
 | 
					      'alert_namespace',
 | 
				
			||||||
 | 
					      'container_namespace',
 | 
				
			||||||
 | 
					      'client_k8s_namespace_name',
 | 
				
			||||||
 | 
					      'client_service_namespace',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should do substring match when needle contains non-ascii characters', () => {
 | 
				
			||||||
 | 
					    const haystack = ['A水', 'AA', 'AB', 'AC', 'BC', 'C', 'CD'];
 | 
				
			||||||
 | 
					    const needle = '水';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result.map((idx) => haystack[idx])).toEqual(['A水']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should handle multiple non-latin characters', () => {
 | 
				
			||||||
 | 
					    const haystack = ['台灣省', '台中市', '台北市', '台南市', '南投縣', '高雄市', '台中第一高級中學'];
 | 
				
			||||||
 | 
					    const needle = '南';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result.map((idx) => haystack[idx])).toEqual(['台南市', '南投縣']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should do substring match when needle contains only symbols', () => {
 | 
				
			||||||
 | 
					    const haystack = ['=', '<=', '>', '!~'];
 | 
				
			||||||
 | 
					    const needle = '=';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result.map((idx) => haystack[idx])).toEqual(['=', '<=']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should handle empty haystack', () => {
 | 
				
			||||||
 | 
					    const haystack: string[] = [];
 | 
				
			||||||
 | 
					    const needle = 'test';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toEqual([]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should handle no matches', () => {
 | 
				
			||||||
 | 
					    const haystack = ['apple', 'banana', 'cherry'];
 | 
				
			||||||
 | 
					    const needle = 'xyz';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toEqual([]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return indices in the correct order', () => {
 | 
				
			||||||
 | 
					    const haystack = ['zebra', 'apple', 'aardvark', 'application'];
 | 
				
			||||||
 | 
					    const needle = 'app';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result.forEach((index) => {
 | 
				
			||||||
 | 
					      expect(haystack[index].toLowerCase()).toContain('app');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should handle partial matches', () => {
 | 
				
			||||||
 | 
					    const haystack = ['Dashboard', 'Dashboards', 'dash-config', 'config-dash'];
 | 
				
			||||||
 | 
					    const needle = 'dash';
 | 
				
			||||||
 | 
					    const result = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result.length).toEqual(haystack.length);
 | 
				
			||||||
 | 
					    result.forEach((index) => {
 | 
				
			||||||
 | 
					      expect(haystack[index].toLowerCase()).toContain('dash');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					import uFuzzy from '@leeoniya/ufuzzy';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://catonmat.net/my-favorite-regex :)
 | 
				
			||||||
 | 
					const REGEXP_NON_ASCII = /[^ -~]/m;
 | 
				
			||||||
 | 
					// https://www.asciitable.com/
 | 
				
			||||||
 | 
					// matches only these: `~!@#$%^&*()_+-=[]\{}|;':",./<>?
 | 
				
			||||||
 | 
					const REGEXP_ONLY_SYMBOLS = /^[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+$/m;
 | 
				
			||||||
 | 
					// limit max terms in needle that qualify for re-ordering
 | 
				
			||||||
 | 
					const outOfOrderLimit = 5;
 | 
				
			||||||
 | 
					// beyond 25 chars fall back to substring search
 | 
				
			||||||
 | 
					const maxNeedleLength = 25;
 | 
				
			||||||
 | 
					// beyond 5 terms fall back to substring match
 | 
				
			||||||
 | 
					const maxFuzzyTerms = 5;
 | 
				
			||||||
 | 
					// when number of matches <= 1e4, do ranking + sorting by quality
 | 
				
			||||||
 | 
					const rankThreshold = 1e4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// typo tolerance mode
 | 
				
			||||||
 | 
					const uf = new uFuzzy({ intraMode: 1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Fuzzy search utility that returns matching indices for a given search term
 | 
				
			||||||
 | 
					 * Uses intelligent fallback strategies for different types of input
 | 
				
			||||||
 | 
					 * @internal
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function fuzzySearch(haystack: string[], needle: string): number[] {
 | 
				
			||||||
 | 
					  if (needle === '') {
 | 
				
			||||||
 | 
					    return haystack.map((_, index) => index);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // fallback to substring matches to avoid badness
 | 
				
			||||||
 | 
					  else if (
 | 
				
			||||||
 | 
					    // contains non-ascii
 | 
				
			||||||
 | 
					    REGEXP_NON_ASCII.test(needle) ||
 | 
				
			||||||
 | 
					    // is only ascii symbols (operators)
 | 
				
			||||||
 | 
					    REGEXP_ONLY_SYMBOLS.test(needle) ||
 | 
				
			||||||
 | 
					    // too long (often copy-paste from somewhere)
 | 
				
			||||||
 | 
					    needle.length > maxNeedleLength ||
 | 
				
			||||||
 | 
					    uf.split(needle).length > maxFuzzyTerms
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const indices: number[] = [];
 | 
				
			||||||
 | 
					    for (let i = 0; i < haystack.length; i++) {
 | 
				
			||||||
 | 
					      let item = haystack[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (item.includes(needle)) {
 | 
				
			||||||
 | 
					        indices.push(i);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return indices;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // fuzzy search
 | 
				
			||||||
 | 
					  else {
 | 
				
			||||||
 | 
					    const [idxs, info, order] = uf.search(haystack, needle, outOfOrderLimit, rankThreshold);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (idxs?.length) {
 | 
				
			||||||
 | 
					      if (info && order) {
 | 
				
			||||||
 | 
					        return order.map((idx) => info.idx[idx]);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return idxs;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,6 @@
 | 
				
			||||||
    "@grafana/i18n": "12.1.0-pre",
 | 
					    "@grafana/i18n": "12.1.0-pre",
 | 
				
			||||||
    "@grafana/schema": "12.1.0-pre",
 | 
					    "@grafana/schema": "12.1.0-pre",
 | 
				
			||||||
    "@hello-pangea/dnd": "18.0.1",
 | 
					    "@hello-pangea/dnd": "18.0.1",
 | 
				
			||||||
    "@leeoniya/ufuzzy": "1.0.18",
 | 
					 | 
				
			||||||
    "@monaco-editor/react": "4.7.0",
 | 
					    "@monaco-editor/react": "4.7.0",
 | 
				
			||||||
    "@popperjs/core": "2.11.8",
 | 
					    "@popperjs/core": "2.11.8",
 | 
				
			||||||
    "@react-aria/dialog": "3.5.27",
 | 
					    "@react-aria/dialog": "3.5.27",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,74 +0,0 @@
 | 
				
			||||||
import { fuzzyFind } from './filter';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('combobox filter', () => {
 | 
					 | 
				
			||||||
  it('should properly rank by match quality', () => {
 | 
					 | 
				
			||||||
    const needle = 'C';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const stringOptions = ['A', 'AA', 'AB', 'AC', 'BC', 'C', 'CD'];
 | 
					 | 
				
			||||||
    const options = stringOptions.map((value) => ({ value }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const matches = fuzzyFind(options, stringOptions, needle);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    expect(matches.map((m) => m.value)).toEqual(['C', 'CD', 'AC', 'BC']);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('orders by match quality and case sensitivty', () => {
 | 
					 | 
				
			||||||
    const stringOptions = [
 | 
					 | 
				
			||||||
      'client_service_namespace',
 | 
					 | 
				
			||||||
      'namespace',
 | 
					 | 
				
			||||||
      'alert_namespace',
 | 
					 | 
				
			||||||
      'container_namespace',
 | 
					 | 
				
			||||||
      'Namespace',
 | 
					 | 
				
			||||||
      'client_k8s_namespace_name',
 | 
					 | 
				
			||||||
      'foobar',
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
    const options = stringOptions.map((value) => ({ value }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const matches = fuzzyFind(options, stringOptions, 'Names');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    expect(matches.map((m) => m.value)).toEqual([
 | 
					 | 
				
			||||||
      'Namespace',
 | 
					 | 
				
			||||||
      'namespace',
 | 
					 | 
				
			||||||
      'alert_namespace',
 | 
					 | 
				
			||||||
      'container_namespace',
 | 
					 | 
				
			||||||
      'client_k8s_namespace_name',
 | 
					 | 
				
			||||||
      'client_service_namespace',
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('non-ascii', () => {
 | 
					 | 
				
			||||||
    it('should do substring match when needle is non-latin', () => {
 | 
					 | 
				
			||||||
      const needle = '水';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const stringOptions = ['A水', 'AA', 'AB', 'AC', 'BC', 'C', 'CD'];
 | 
					 | 
				
			||||||
      const options = stringOptions.map((value) => ({ value }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const matches = fuzzyFind(options, stringOptions, needle);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(matches.map((m) => m.value)).toEqual(['A水']);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('second case for non-latin characters', () => {
 | 
					 | 
				
			||||||
      const stringOptions = ['台灣省', '台中市', '台北市', '台南市', '南投縣', '高雄市', '台中第一高級中學'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const options = stringOptions.map((value) => ({ value }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const matches = fuzzyFind(options, stringOptions, '南');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(matches.map((m) => m.value)).toEqual(['台南市', '南投縣']);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('operators', () => {
 | 
					 | 
				
			||||||
    it('should do substring match when needle is only symbols', () => {
 | 
					 | 
				
			||||||
      const needle = '=';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const stringOptions = ['=', '<=', '>', '!~'];
 | 
					 | 
				
			||||||
      const options = stringOptions.map((value) => ({ value }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const matches = fuzzyFind(options, stringOptions, needle);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(matches.map((m) => m.value)).toEqual(['=', '<=']);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,6 @@
 | 
				
			||||||
import uFuzzy from '@leeoniya/ufuzzy';
 | 
					import { fuzzySearch } from '@grafana/data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ALL_OPTION_VALUE, ComboboxOption } from './types';
 | 
					import { ComboboxOption } from './types';
 | 
				
			||||||
 | 
					 | 
				
			||||||
// https://catonmat.net/my-favorite-regex :)
 | 
					 | 
				
			||||||
const REGEXP_NON_ASCII = /[^ -~]/m;
 | 
					 | 
				
			||||||
// https://www.asciitable.com/
 | 
					 | 
				
			||||||
// matches only these: `~!@#$%^&*()_+-=[]\{}|;':",./<>?
 | 
					 | 
				
			||||||
const REGEXP_ONLY_SYMBOLS = /^[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+$/m;
 | 
					 | 
				
			||||||
// limit max terms in needle that qualify for re-ordering
 | 
					 | 
				
			||||||
const outOfOrderLimit = 5;
 | 
					 | 
				
			||||||
// beyond 25 chars fall back to substring search
 | 
					 | 
				
			||||||
const maxNeedleLength = 25;
 | 
					 | 
				
			||||||
// beyond 5 terms fall back to substring match
 | 
					 | 
				
			||||||
const maxFuzzyTerms = 5;
 | 
					 | 
				
			||||||
// when number of matches <= 1e4, do ranking + sorting by quality
 | 
					 | 
				
			||||||
const rankThreshold = 1e4;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// typo tolerance mode
 | 
					 | 
				
			||||||
const uf = new uFuzzy({ intraMode: 1 });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function itemToString<T extends string | number>(item?: ComboboxOption<T> | null) {
 | 
					export function itemToString<T extends string | number>(item?: ComboboxOption<T> | null) {
 | 
				
			||||||
  if (item == null) {
 | 
					  if (item == null) {
 | 
				
			||||||
| 
						 | 
					@ -26,60 +9,11 @@ export function itemToString<T extends string | number>(item?: ComboboxOption<T>
 | 
				
			||||||
  return item.label ?? item.value.toString();
 | 
					  return item.label ?? item.value.toString();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//TODO: Remove when MutliCombobox async has been merged
 | 
					 | 
				
			||||||
export function itemFilter<T extends string | number>(inputValue: string) {
 | 
					 | 
				
			||||||
  const lowerCasedInputValue = inputValue.toLowerCase();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (item: ComboboxOption<T>) => {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      !inputValue ||
 | 
					 | 
				
			||||||
      item.label?.toLowerCase().includes(lowerCasedInputValue) ||
 | 
					 | 
				
			||||||
      item.value?.toString().toLowerCase().includes(lowerCasedInputValue) ||
 | 
					 | 
				
			||||||
      item.value.toString() === ALL_OPTION_VALUE
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function fuzzyFind<T extends string | number>(
 | 
					export function fuzzyFind<T extends string | number>(
 | 
				
			||||||
  options: Array<ComboboxOption<T>>,
 | 
					  options: Array<ComboboxOption<T>>,
 | 
				
			||||||
  haystack: string[],
 | 
					  haystack: string[],
 | 
				
			||||||
  needle: string
 | 
					  needle: string
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  let matches: Array<ComboboxOption<T>> = [];
 | 
					  const indices = fuzzySearch(haystack, needle);
 | 
				
			||||||
 | 
					  return indices.map((idx) => options[idx]);
 | 
				
			||||||
  if (needle === '') {
 | 
					 | 
				
			||||||
    matches = options;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // fallback to substring matches to avoid badness
 | 
					 | 
				
			||||||
  else if (
 | 
					 | 
				
			||||||
    // contains non-ascii
 | 
					 | 
				
			||||||
    REGEXP_NON_ASCII.test(needle) ||
 | 
					 | 
				
			||||||
    // is only ascii symbols (operators)
 | 
					 | 
				
			||||||
    REGEXP_ONLY_SYMBOLS.test(needle) ||
 | 
					 | 
				
			||||||
    // too long (often copy-paste from somewhere)
 | 
					 | 
				
			||||||
    needle.length > maxNeedleLength ||
 | 
					 | 
				
			||||||
    uf.split(needle).length > maxFuzzyTerms
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    for (let i = 0; i < haystack.length; i++) {
 | 
					 | 
				
			||||||
      let item = haystack[i];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (item.includes(needle)) {
 | 
					 | 
				
			||||||
        matches.push(options[i]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // fuzzy search
 | 
					 | 
				
			||||||
  else {
 | 
					 | 
				
			||||||
    const [idxs, info, order] = uf.search(haystack, needle, outOfOrderLimit, rankThreshold);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (idxs?.length) {
 | 
					 | 
				
			||||||
      if (info && order) {
 | 
					 | 
				
			||||||
        matches = order.map((idx) => options[info.idx[idx]]);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        matches = idxs.map((idx) => options[idx]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return matches;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3021,6 +3021,7 @@ __metadata:
 | 
				
			||||||
    "@braintree/sanitize-url": "npm:7.0.1"
 | 
					    "@braintree/sanitize-url": "npm:7.0.1"
 | 
				
			||||||
    "@grafana/schema": "npm:12.1.0-pre"
 | 
					    "@grafana/schema": "npm:12.1.0-pre"
 | 
				
			||||||
    "@grafana/tsconfig": "npm:^2.0.0"
 | 
					    "@grafana/tsconfig": "npm:^2.0.0"
 | 
				
			||||||
 | 
					    "@leeoniya/ufuzzy": "npm:1.0.18"
 | 
				
			||||||
    "@rollup/plugin-node-resolve": "npm:16.0.0"
 | 
					    "@rollup/plugin-node-resolve": "npm:16.0.0"
 | 
				
			||||||
    "@types/d3-interpolate": "npm:^3.0.0"
 | 
					    "@types/d3-interpolate": "npm:^3.0.0"
 | 
				
			||||||
    "@types/history": "npm:4.7.11"
 | 
					    "@types/history": "npm:4.7.11"
 | 
				
			||||||
| 
						 | 
					@ -3643,7 +3644,6 @@ __metadata:
 | 
				
			||||||
    "@grafana/schema": "npm:12.1.0-pre"
 | 
					    "@grafana/schema": "npm:12.1.0-pre"
 | 
				
			||||||
    "@grafana/tsconfig": "npm:^2.0.0"
 | 
					    "@grafana/tsconfig": "npm:^2.0.0"
 | 
				
			||||||
    "@hello-pangea/dnd": "npm:18.0.1"
 | 
					    "@hello-pangea/dnd": "npm:18.0.1"
 | 
				
			||||||
    "@leeoniya/ufuzzy": "npm:1.0.18"
 | 
					 | 
				
			||||||
    "@monaco-editor/react": "npm:4.7.0"
 | 
					    "@monaco-editor/react": "npm:4.7.0"
 | 
				
			||||||
    "@popperjs/core": "npm:2.11.8"
 | 
					    "@popperjs/core": "npm:2.11.8"
 | 
				
			||||||
    "@react-aria/dialog": "npm:3.5.27"
 | 
					    "@react-aria/dialog": "npm:3.5.27"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue