diff --git a/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.test.tsx b/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.test.tsx
new file mode 100644
index 00000000000..1ee7c4a4281
--- /dev/null
+++ b/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.test.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { PartialHighlighter } from './PartialHighlighter';
+
+function assertPart(component: ReactWrapper, isHighlighted: boolean, text: string): void {
+ expect(component.type()).toEqual(isHighlighted ? 'mark' : 'span');
+ expect(component.hasClass('highlight')).toEqual(isHighlighted);
+ expect(component.text()).toEqual(text);
+}
+
+describe('PartialHighlighter component', () => {
+ it('should highlight inner parts correctly', () => {
+ const component = mount(
+
+ );
+ const main = component.find('div');
+
+ assertPart(main.childAt(0), false, 'Lorem ');
+ assertPart(main.childAt(1), true, 'ipsum');
+ assertPart(main.childAt(2), false, ' dolor ');
+ assertPart(main.childAt(3), true, 'sit');
+ assertPart(main.childAt(4), false, ' amet');
+ });
+
+ it('should highlight outer parts correctly', () => {
+ const component = mount(
+
+ );
+ const main = component.find('div');
+ assertPart(main.childAt(0), true, 'Lorem');
+ assertPart(main.childAt(1), false, ' ipsum dolor sit ');
+ assertPart(main.childAt(2), true, 'amet');
+ });
+});
diff --git a/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.tsx b/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.tsx
new file mode 100644
index 00000000000..c97b86b96ea
--- /dev/null
+++ b/packages/grafana-ui/src/components/Typeahead/PartialHighlighter.tsx
@@ -0,0 +1,55 @@
+import React, { createElement } from 'react';
+import { HighlightPart } from '../../types';
+
+interface Props {
+ text: string;
+ highlightParts: HighlightPart[];
+ highlightClassName: string;
+}
+
+/**
+ * Flattens parts into a list of indices pointing to the index where a part
+ * (highlighted or not highlighted) starts. Adds extra indices if needed
+ * at the beginning or the end to ensure the entire text is covered.
+ */
+function getStartIndices(parts: HighlightPart[], length: number): number[] {
+ const indices: number[] = [];
+ parts.forEach((part) => {
+ indices.push(part.start, part.end + 1);
+ });
+ if (indices[0] !== 0) {
+ indices.unshift(0);
+ }
+ if (indices[indices.length - 1] !== length) {
+ indices.push(length);
+ }
+ return indices;
+}
+
+export const PartialHighlighter: React.FC = (props: Props) => {
+ let { highlightParts, text, highlightClassName } = props;
+
+ if (!highlightParts) {
+ return null;
+ }
+
+ let children = [];
+ let indices = getStartIndices(highlightParts, text.length);
+ let highlighted = highlightParts[0].start === 0;
+
+ for (let i = 1; i < indices.length; i++) {
+ let start = indices[i - 1];
+ let end = indices[i];
+
+ children.push(
+ createElement(highlighted ? 'mark' : 'span', {
+ key: i - 1,
+ children: text.substring(start, end),
+ className: highlighted ? highlightClassName : undefined,
+ })
+ );
+ highlighted = !highlighted;
+ }
+
+ return {children}
;
+};
diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx
index 2d6a16e62a1..fbe81f7dec8 100644
--- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx
+++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx
@@ -6,6 +6,7 @@ import { css, cx } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { CompletionItem, CompletionItemKind } from '../../types/completion';
import { ThemeContext } from '../../themes/ThemeContext';
+import { PartialHighlighter } from './PartialHighlighter';
interface Props {
isSelected: boolean;
@@ -83,7 +84,15 @@ export const TypeaheadItem: React.FC = (props: Props) => {
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
-
+ {item.highlightParts !== undefined ? (
+
+ ) : (
+
+ )}
);
};
diff --git a/packages/grafana-ui/src/slate-plugins/fuzzy.test.ts b/packages/grafana-ui/src/slate-plugins/fuzzy.test.ts
new file mode 100644
index 00000000000..c32a848edd7
--- /dev/null
+++ b/packages/grafana-ui/src/slate-plugins/fuzzy.test.ts
@@ -0,0 +1,79 @@
+import { fuzzyMatch } from './fuzzy';
+
+describe('Fuzzy search', () => {
+ it('finds only matching elements', () => {
+ expect(fuzzyMatch('foo', 'foo')).toEqual({
+ distance: 0,
+ ranges: [{ start: 0, end: 2 }],
+ found: true,
+ });
+
+ expect(fuzzyMatch('foo_bar', 'foo')).toEqual({
+ distance: 0,
+ ranges: [{ start: 0, end: 2 }],
+ found: true,
+ });
+
+ expect(fuzzyMatch('bar', 'foo')).toEqual({
+ distance: Infinity,
+ ranges: [],
+ found: false,
+ });
+ });
+
+ it('is case sensitive', () => {
+ expect(fuzzyMatch('foo_bar', 'BAR')).toEqual({
+ distance: Infinity,
+ ranges: [],
+ found: false,
+ });
+ expect(fuzzyMatch('Foo_Bar', 'bar')).toEqual({
+ distance: Infinity,
+ ranges: [],
+ found: false,
+ });
+ });
+
+ it('finds highlight ranges with single letters', () => {
+ expect(fuzzyMatch('foo_xyzzy_bar', 'fxb')).toEqual({
+ ranges: [
+ { start: 0, end: 0 },
+ { start: 4, end: 4 },
+ { start: 10, end: 10 },
+ ],
+ distance: 8,
+ found: true,
+ });
+ });
+
+ it('finds highlight ranges for multiple outer words', () => {
+ expect(fuzzyMatch('foo_xyzzy_bar', 'foobar')).toEqual({
+ ranges: [
+ { start: 0, end: 2 },
+ { start: 10, end: 12 },
+ ],
+ distance: 7,
+ found: true,
+ });
+ });
+
+ it('finds highlight ranges for multiple inner words', () => {
+ expect(fuzzyMatch('foo_xyzzy_bar', 'oozzyba')).toEqual({
+ ranges: [
+ { start: 1, end: 2 },
+ { start: 6, end: 8 },
+ { start: 10, end: 11 },
+ ],
+ distance: 4,
+ found: true,
+ });
+ });
+
+ it('promotes exact matches', () => {
+ expect(fuzzyMatch('bbaarr_bar_bbaarr', 'bar')).toEqual({
+ ranges: [{ start: 7, end: 9 }],
+ distance: 0,
+ found: true,
+ });
+ });
+});
diff --git a/packages/grafana-ui/src/slate-plugins/fuzzy.ts b/packages/grafana-ui/src/slate-plugins/fuzzy.ts
new file mode 100644
index 00000000000..030831defaa
--- /dev/null
+++ b/packages/grafana-ui/src/slate-plugins/fuzzy.ts
@@ -0,0 +1,67 @@
+import { HighlightPart } from '../types';
+import { last } from 'lodash';
+
+type FuzzyMatch = {
+ /**
+ * Total number of unmatched letters between matched letters
+ */
+ distance: number;
+ ranges: HighlightPart[];
+ found: boolean;
+};
+
+/**
+ * Attempts to do a partial input search, e.g. allowing to search for a text (needle)
+ * in another text (stack) by skipping some letters in-between. All letters from
+ * the needle must exist in the stack in the same order to find a match.
+ *
+ * The search is case sensitive. Convert stack and needle to lower case
+ * to make it case insensitive.
+ *
+ * @param stack - main text to be searched
+ * @param needle - partial text to find in the stack
+ */
+export function fuzzyMatch(stack: string, needle: string): FuzzyMatch {
+ let distance = 0,
+ searchIndex = stack.indexOf(needle);
+
+ const ranges: HighlightPart[] = [];
+
+ if (searchIndex !== -1) {
+ return {
+ distance: 0,
+ found: true,
+ ranges: [{ start: searchIndex, end: searchIndex + needle.length - 1 }],
+ };
+ }
+
+ for (const letter of needle) {
+ const letterIndex = stack.indexOf(letter, searchIndex);
+
+ if (letterIndex === -1) {
+ return { distance: Infinity, ranges: [], found: false };
+ }
+ // do not cumulate the distance if it's the first letter
+ if (searchIndex !== -1) {
+ distance += letterIndex - searchIndex;
+ }
+ searchIndex = letterIndex + 1;
+
+ if (ranges.length === 0) {
+ ranges.push({ start: letterIndex, end: letterIndex });
+ } else {
+ const lastRange = last(ranges)!;
+ if (letterIndex === lastRange.end + 1) {
+ lastRange.end++;
+ } else {
+ ranges.push({ start: letterIndex, end: letterIndex });
+ }
+ }
+ }
+
+ return {
+ distance: distance,
+ ranges,
+ found: true,
+ };
+}
diff --git a/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx
new file mode 100644
index 00000000000..a250a992032
--- /dev/null
+++ b/packages/grafana-ui/src/slate-plugins/suggestions.test.tsx
@@ -0,0 +1,156 @@
+import { SearchFunctionMap } from '../utils/searchFunctions';
+import { render } from 'enzyme';
+import { SuggestionsPlugin } from './suggestions';
+import { Plugin as SlatePlugin } from '@grafana/slate-react';
+import { SearchFunctionType } from '../utils';
+import { CompletionItemGroup, SuggestionsState } from '../types';
+
+jest.mock('../utils/searchFunctions', () => ({
+ // @ts-ignore
+ ...jest.requireActual('../utils/searchFunctions'),
+ SearchFunctionMap: {
+ Prefix: jest.fn((items) => items),
+ Word: jest.fn((items) => items),
+ Fuzzy: jest.fn((items) => items),
+ },
+}));
+
+const TypeaheadMock = jest.fn(() => '');
+jest.mock('../components/Typeahead/Typeahead', () => {
+ return {
+ Typeahead: (state: Partial) => {
+ // @ts-ignore
+ TypeaheadMock(state);
+ return '';
+ },
+ };
+});
+
+jest.mock('lodash/debounce', () => {
+ return (func: () => any) => func;
+});
+
+describe('SuggestionsPlugin', () => {
+ let plugin: SlatePlugin, nextMock: any, suggestions: CompletionItemGroup[], editorMock: any, eventMock: any;
+
+ beforeEach(() => {
+ let onTypeahead = async () => {
+ return {
+ suggestions: suggestions,
+ };
+ };
+
+ (SearchFunctionMap.Prefix as jest.Mock).mockClear();
+ (SearchFunctionMap.Word as jest.Mock).mockClear();
+ (SearchFunctionMap.Fuzzy as jest.Mock).mockClear();
+
+ plugin = SuggestionsPlugin({ portalOrigin: '', onTypeahead });
+ nextMock = () => {};
+ editorMock = createEditorMock('foo');
+ eventMock = new window.KeyboardEvent('keydown', { key: 'a' });
+ });
+
+ async function triggerAutocomplete() {
+ await plugin.onKeyDown!(eventMock, editorMock, nextMock);
+ render(plugin.renderEditor!({} as any, editorMock, nextMock));
+ }
+
+ it('is backward compatible with prefixMatch and sortText', async () => {
+ suggestions = [
+ {
+ label: 'group',
+ prefixMatch: true,
+ items: [
+ { label: 'foobar', sortText: '3' },
+ { label: 'foobar', sortText: '1' },
+ { label: 'foobar', sortText: '2' },
+ ],
+ },
+ ];
+
+ await triggerAutocomplete();
+
+ expect(SearchFunctionMap.Word).not.toBeCalled();
+ expect(SearchFunctionMap.Fuzzy).not.toBeCalled();
+ expect(SearchFunctionMap.Prefix).toBeCalled();
+
+ expect(TypeaheadMock).toBeCalledWith(
+ expect.objectContaining({
+ groupedItems: [
+ {
+ label: 'group',
+ prefixMatch: true,
+ items: [
+ { label: 'foobar', sortText: '1' },
+ { label: 'foobar', sortText: '2' },
+ { label: 'foobar', sortText: '3' },
+ ],
+ },
+ ],
+ })
+ );
+ });
+
+ it('uses searchFunction to create autocomplete list and sortValue if defined', async () => {
+ suggestions = [
+ {
+ label: 'group',
+ searchFunctionType: SearchFunctionType.Fuzzy,
+ items: [
+ { label: 'foobar', sortValue: 3 },
+ { label: 'foobar', sortValue: 1 },
+ { label: 'foobar', sortValue: 2 },
+ ],
+ },
+ ];
+
+ await triggerAutocomplete();
+
+ expect(SearchFunctionMap.Word).not.toBeCalled();
+ expect(SearchFunctionMap.Prefix).not.toBeCalled();
+ expect(SearchFunctionMap.Fuzzy).toBeCalled();
+
+ expect(TypeaheadMock).toBeCalledWith(
+ expect.objectContaining({
+ groupedItems: [
+ {
+ label: 'group',
+ searchFunctionType: SearchFunctionType.Fuzzy,
+ items: [
+ { label: 'foobar', sortValue: 1 },
+ { label: 'foobar', sortValue: 2 },
+ { label: 'foobar', sortValue: 3 },
+ ],
+ },
+ ],
+ })
+ );
+ });
+});
+
+function createEditorMock(currentText: string) {
+ return {
+ blur: jest.fn().mockReturnThis(),
+ focus: jest.fn().mockReturnThis(),
+ value: {
+ selection: {
+ start: {
+ offset: 0,
+ },
+ end: {
+ offset: 0,
+ },
+ focus: {
+ offset: currentText.length,
+ },
+ },
+ document: {
+ getClosestBlock: () => {},
+ },
+ focusText: {
+ text: currentText,
+ },
+ focusBlock: {},
+ },
+ };
+}
diff --git a/packages/grafana-ui/src/slate-plugins/suggestions.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.tsx
index bfd821ba0a8..49b13e0de85 100644
--- a/packages/grafana-ui/src/slate-plugins/suggestions.tsx
+++ b/packages/grafana-ui/src/slate-plugins/suggestions.tsx
@@ -7,8 +7,9 @@ import { Plugin as SlatePlugin } from '@grafana/slate-react';
import TOKEN_MARK from './slate-prism/TOKEN_MARK';
import { Typeahead } from '../components/Typeahead/Typeahead';
-import { CompletionItem, TypeaheadOutput, TypeaheadInput, SuggestionsState } from '../types/completion';
-import { makeFragment } from '../utils/slate';
+import { CompletionItem, SuggestionsState, TypeaheadInput, TypeaheadOutput } from '../types';
+import { makeFragment, SearchFunctionType } from '../utils';
+import { SearchFunctionMap } from '../utils/searchFunctions';
export const TYPEAHEAD_DEBOUNCE = 250;
@@ -289,17 +290,16 @@ const handleTypeahead = async (
if (!group.items) {
return group;
}
-
+ // Falling back to deprecated prefixMatch to support backwards compatibility with plugins using this property
+ const searchFunctionType =
+ group.searchFunctionType || (group.prefixMatch ? SearchFunctionType.Prefix : SearchFunctionType.Word);
+ const searchFunction = SearchFunctionMap[searchFunctionType];
let newGroup = { ...group };
if (prefix) {
// Filter groups based on prefix
if (!group.skipFilter) {
newGroup.items = newGroup.items.filter((c) => (c.filterText || c.label).length >= prefix.length);
- if (group.prefixMatch) {
- newGroup.items = newGroup.items.filter((c) => (c.filterText || c.label).startsWith(prefix));
- } else {
- newGroup.items = newGroup.items.filter((c) => (c.filterText || c.label).includes(prefix));
- }
+ newGroup.items = searchFunction(newGroup.items, prefix);
}
// Filter out the already typed value (prefix) unless it inserts custom text not matching the prefix
@@ -309,7 +309,14 @@ const handleTypeahead = async (
}
if (!group.skipSort) {
- newGroup.items = sortBy(newGroup.items, (item: CompletionItem) => item.sortText || item.label);
+ newGroup.items = sortBy(newGroup.items, (item: CompletionItem) => {
+ if (item.sortText === undefined) {
+ return item.sortValue !== undefined ? item.sortValue : item.label;
+ } else {
+ // Falling back to deprecated sortText to support backwards compatibility with plugins using this property
+ return item.sortText || item.label;
+ }
+ });
}
return newGroup;
diff --git a/packages/grafana-ui/src/types/completion.ts b/packages/grafana-ui/src/types/completion.ts
index b56c360faa5..1855889ff1e 100644
--- a/packages/grafana-ui/src/types/completion.ts
+++ b/packages/grafana-ui/src/types/completion.ts
@@ -1,4 +1,10 @@
import { Value, Editor as CoreEditor } from 'slate';
+import { SearchFunctionType } from '../utils';
+
+/**
+ * @internal
+ */
+export type SearchFunction = (items: CompletionItem[], prefix: string) => CompletionItem[];
export interface CompletionItemGroup {
/**
@@ -13,9 +19,16 @@ export interface CompletionItemGroup {
/**
* If true, match only by prefix (and not mid-word).
+ * @deprecated use searchFunctionType instead
*/
prefixMatch?: boolean;
+ /**
+ * Function type used to create auto-complete list
+ * @alpha
+ */
+ searchFunctionType?: SearchFunctionType;
+
/**
* If true, do not filter items in this group based on the search.
*/
@@ -31,6 +44,14 @@ export enum CompletionItemKind {
GroupTitle = 'GroupTitle',
}
+/**
+ * @internal
+ */
+export type HighlightPart = {
+ start: number;
+ end: number;
+};
+
export interface CompletionItem {
/**
* The label of this completion item. By default
@@ -59,9 +80,23 @@ export interface CompletionItem {
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the `label` is used.
+ * @deprecated use sortValue instead
*/
sortText?: string;
+ /**
+ * A string or number that should be used when comparing this
+ * item with other items. When `undefined` then `label` is used.
+ * @alpha
+ */
+ sortValue?: string | number;
+
+ /**
+ * Parts of the label to be highlighted
+ * @internal
+ */
+ highlightParts?: HighlightPart[];
+
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the `label` is used.
diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts
index 63a19b03ddb..81f1fc7c533 100644
--- a/packages/grafana-ui/src/utils/index.ts
+++ b/packages/grafana-ui/src/utils/index.ts
@@ -6,6 +6,7 @@ export * from './tags';
export * from './scrollbar';
export * from './measureText';
export * from './useForceUpdate';
+export { SearchFunctionType } from './searchFunctions';
export { default as ansicolor } from './ansicolor';
import * as DOMUtil from './dom'; // includes Element.closest polyfill
diff --git a/packages/grafana-ui/src/utils/searchFunctions.ts b/packages/grafana-ui/src/utils/searchFunctions.ts
new file mode 100644
index 00000000000..9ceb1fd89be
--- /dev/null
+++ b/packages/grafana-ui/src/utils/searchFunctions.ts
@@ -0,0 +1,58 @@
+import { CompletionItem, SearchFunction } from '../types';
+import { fuzzyMatch } from '../slate-plugins/fuzzy';
+
+/**
+ * List of auto-complete search function used by SuggestionsPlugin.handleTypeahead()
+ * @alpha
+ */
+export enum SearchFunctionType {
+ Word = 'Word',
+ Prefix = 'Prefix',
+ Fuzzy = 'Fuzzy',
+}
+
+/**
+ * Exact-word matching for auto-complete suggestions.
+ * - Returns items containing the searched text.
+ * @internal
+ */
+const wordSearch: SearchFunction = (items: CompletionItem[], text: string): CompletionItem[] => {
+ return items.filter((c) => (c.filterText || c.label).includes(text));
+};
+
+/**
+ * Prefix-based search for auto-complete suggestions.
+ * - Returns items starting with the searched text.
+ * @internal
+ */
+const prefixSearch: SearchFunction = (items: CompletionItem[], text: string): CompletionItem[] => {
+ return items.filter((c) => (c.filterText || c.label).startsWith(text));
+};
+
+/**
+ * Fuzzy search for auto-complete suggestions.
+ * - Returns items containing all letters from the search text occurring in the same order.
+ * - Stores highlight parts with parts of the text phrase found by fuzzy search
+ * @internal
+ */
+const fuzzySearch: SearchFunction = (items: CompletionItem[], text: string): CompletionItem[] => {
+ text = text.toLowerCase();
+ return items.filter((item) => {
+ const { distance, ranges, found } = fuzzyMatch(item.label.toLowerCase(), text);
+ if (!found) {
+ return false;
+ }
+ item.sortValue = distance;
+ item.highlightParts = ranges;
+ return true;
+ });
+};
+
+/**
+ * @internal
+ */
+export const SearchFunctionMap = {
+ [SearchFunctionType.Word]: wordSearch,
+ [SearchFunctionType.Prefix]: prefixSearch,
+ [SearchFunctionType.Fuzzy]: fuzzySearch,
+};
diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.ts b/public/app/plugins/datasource/cloudwatch/language_provider.ts
index 004ee9e739b..601247f9612 100644
--- a/public/app/plugins/datasource/cloudwatch/language_provider.ts
+++ b/public/app/plugins/datasource/cloudwatch/language_provider.ts
@@ -18,7 +18,7 @@ import { CloudWatchQuery, TSDBResponse } from './types';
import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data';
import { CloudWatchDatasource } from './datasource';
-import { Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
+import { CompletionItemGroup, SearchFunctionType, Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import Prism, { Grammar } from 'prismjs';
export type CloudWatchHistoryItem = HistoryItem;
@@ -167,8 +167,12 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
private handleKeyword = async (context?: TypeaheadContext): Promise => {
const suggs = await this.getFieldCompletionItems(context?.logGroupNames ?? []);
- const functionSuggestions = [
- { prefixMatch: true, label: 'Functions', items: STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS) },
+ const functionSuggestions: CompletionItemGroup[] = [
+ {
+ searchFunctionType: SearchFunctionType.Prefix,
+ label: 'Functions',
+ items: STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS),
+ },
];
suggs.suggestions.push(...functionSuggestions);
@@ -244,7 +248,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
return {
suggestions: [
{
- prefixMatch: true,
+ searchFunctionType: SearchFunctionType.Prefix,
label: 'Sort Order',
items: [
{
@@ -268,22 +272,32 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
};
private getCommandCompletionItems = (): TypeaheadOutput => {
- return { suggestions: [{ prefixMatch: true, label: 'Commands', items: QUERY_COMMANDS }] };
+ return {
+ suggestions: [{ searchFunctionType: SearchFunctionType.Prefix, label: 'Commands', items: QUERY_COMMANDS }],
+ };
};
private getFieldAndFilterFunctionCompletionItems = (): TypeaheadOutput => {
- return { suggestions: [{ prefixMatch: true, label: 'Functions', items: FIELD_AND_FILTER_FUNCTIONS }] };
+ return {
+ suggestions: [
+ { searchFunctionType: SearchFunctionType.Prefix, label: 'Functions', items: FIELD_AND_FILTER_FUNCTIONS },
+ ],
+ };
};
private getStatsAggCompletionItems = (): TypeaheadOutput => {
- return { suggestions: [{ prefixMatch: true, label: 'Functions', items: AGGREGATION_FUNCTIONS_STATS }] };
+ return {
+ suggestions: [
+ { searchFunctionType: SearchFunctionType.Prefix, label: 'Functions', items: AGGREGATION_FUNCTIONS_STATS },
+ ],
+ };
};
private getBoolFuncCompletionItems = (): TypeaheadOutput => {
return {
suggestions: [
{
- prefixMatch: true,
+ searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: BOOLEAN_FUNCTIONS,
},
@@ -295,7 +309,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
return {
suggestions: [
{
- prefixMatch: true,
+ searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: NUMERIC_OPERATORS.concat(BOOLEAN_FUNCTIONS),
},
diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts
index fc2e720ccd6..e10cc3f2898 100644
--- a/public/app/plugins/datasource/loki/language_provider.ts
+++ b/public/app/plugins/datasource/loki/language_provider.ts
@@ -29,13 +29,13 @@ const NS_IN_MS = 1000000;
// When changing RATE_RANGES, check if Prometheus/PromQL ranges should be changed too
// @see public/app/plugins/datasource/prometheus/promql.ts
const RATE_RANGES: CompletionItem[] = [
- { label: '$__interval', sortText: '$__interval' },
- { label: '1m', sortText: '00:01:00' },
- { label: '5m', sortText: '00:05:00' },
- { label: '10m', sortText: '00:10:00' },
- { label: '30m', sortText: '00:30:00' },
- { label: '1h', sortText: '01:00:00' },
- { label: '1d', sortText: '24:00:00' },
+ { label: '$__interval', sortValue: '$__interval' },
+ { label: '1m', sortValue: '00:01:00' },
+ { label: '5m', sortValue: '00:05:00' },
+ { label: '10m', sortValue: '00:10:00' },
+ { label: '30m', sortValue: '00:30:00' },
+ { label: '1h', sortValue: '01:00:00' },
+ { label: '1d', sortValue: '24:00:00' },
];
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts
index 6109e2ed231..2bbc9ddf89c 100644
--- a/public/app/plugins/datasource/prometheus/language_provider.test.ts
+++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts
@@ -5,6 +5,7 @@ import { PrometheusDatasource } from './datasource';
import { HistoryItem } from '@grafana/data';
import { PromQuery } from './types';
import Mock = jest.Mock;
+import { SearchFunctionType } from '@grafana/ui';
describe('Language completion provider', () => {
const datasource: PrometheusDatasource = ({
@@ -123,14 +124,14 @@ describe('Language completion provider', () => {
expect(result.suggestions).toMatchObject([
{
items: [
- { label: '$__interval', sortText: '$__interval' }, // TODO: figure out why this row and sortText is needed
- { label: '$__rate_interval', sortText: '$__rate_interval' },
- { label: '1m', sortText: '00:01:00' },
- { label: '5m', sortText: '00:05:00' },
- { label: '10m', sortText: '00:10:00' },
- { label: '30m', sortText: '00:30:00' },
- { label: '1h', sortText: '01:00:00' },
- { label: '1d', sortText: '24:00:00' },
+ { label: '$__interval', sortValue: '$__interval' }, // TODO: figure out why this row and sortValue is needed
+ { label: '$__rate_interval', sortValue: '$__rate_interval' },
+ { label: '1m', sortValue: '00:01:00' },
+ { label: '5m', sortValue: '00:05:00' },
+ { label: '10m', sortValue: '00:10:00' },
+ { label: '30m', sortValue: '00:30:00' },
+ { label: '1h', sortValue: '01:00:00' },
+ { label: '1d', sortValue: '24:00:00' },
],
label: 'Range vector',
},
@@ -236,7 +237,13 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
+ expect(result.suggestions).toEqual([
+ {
+ items: [{ label: 'job' }, { label: 'instance' }],
+ label: 'Labels',
+ searchFunctionType: SearchFunctionType.Fuzzy,
+ },
+ ]);
});
it('returns label suggestions on label context and metric', async () => {
@@ -255,7 +262,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+ expect(result.suggestions).toEqual([
+ { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
+ ]);
});
it('returns label suggestions on label context but leaves out labels that already exist', async () => {
@@ -286,7 +295,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+ expect(result.suggestions).toEqual([
+ { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
+ ]);
});
it('returns label value suggestions inside a label value context after a negated matching operator', async () => {
@@ -311,6 +322,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'value1' }, { label: 'value2' }],
label: 'Label values for "job"',
+ searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -346,7 +358,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
- expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
+ expect(result.suggestions).toEqual([
+ { items: [{ label: 'baz' }], label: 'Label values for "bar"', searchFunctionType: SearchFunctionType.Fuzzy },
+ ]);
});
it('returns label suggestions on aggregation context and metric w/ selector', async () => {
@@ -364,7 +378,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
- expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+ expect(result.suggestions).toEqual([
+ { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
+ ]);
});
it('returns label suggestions on aggregation context and metric w/o selector', async () => {
@@ -382,7 +398,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
- expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+ expect(result.suggestions).toEqual([
+ { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
+ ]);
});
it('returns label suggestions inside a multi-line aggregation context', async () => {
@@ -406,6 +424,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
+ searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -429,6 +448,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
+ searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -452,6 +472,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
+ searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -490,6 +511,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
+ searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts
index 95a23c79759..417b16d9b38 100644
--- a/public/app/plugins/datasource/prometheus/language_provider.ts
+++ b/public/app/plugins/datasource/prometheus/language_provider.ts
@@ -3,16 +3,16 @@ import LRU from 'lru-cache';
import { Value } from 'slate';
import { dateTime, HistoryItem, LanguageProvider } from '@grafana/data';
-import { CompletionItem, CompletionItemGroup, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
+import { CompletionItem, CompletionItemGroup, SearchFunctionType, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import {
+ addLimitInfo,
fixSummariesMetadata,
+ limitSuggestions,
parseSelector,
processHistogramLabels,
processLabels,
roundSecToMin,
- addLimitInfo,
- limitSuggestions,
} from './language_utils';
import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';
@@ -201,7 +201,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
getEmptyCompletionItems = (context: { history: Array> }): TypeaheadOutput => {
const { history } = context;
- const suggestions = [];
+ const suggestions: CompletionItemGroup[] = [];
if (history && history.length) {
const historyItems = _.chain(history)
@@ -214,7 +214,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
.value();
suggestions.push({
- prefixMatch: true,
+ searchFunctionType: SearchFunctionType.Prefix,
skipSort: true,
label: 'History',
items: historyItems,
@@ -226,10 +226,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
getTermCompletionItems = (): TypeaheadOutput => {
const { metrics, metricsMetadata } = this;
- const suggestions = [];
+ const suggestions: CompletionItemGroup[] = [];
suggestions.push({
- prefixMatch: true,
+ searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: FUNCTIONS.map(setFunctionKind),
});
@@ -239,6 +239,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Metrics${limitInfo}`,
items: limitSuggestions(metrics).map((m) => addMetricsMetadata(m, metricsMetadata)),
+ searchFunctionType: SearchFunctionType.Fuzzy,
});
}
@@ -313,6 +314,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Labels${limitInfo}`,
items: Object.keys(labelValues).map(wrapLabel),
+ searchFunctionType: SearchFunctionType.Fuzzy,
});
}
return result;
@@ -379,6 +381,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Label values for "${labelKey}"${limitInfo}`,
items: labelValues[labelKey].map(wrapLabel),
+ searchFunctionType: SearchFunctionType.Fuzzy,
});
}
} else {
@@ -391,7 +394,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
context = 'context-labels';
const newItems = possibleKeys.map((key) => ({ label: key }));
const limitInfo = addLimitInfo(newItems);
- const newSuggestion: CompletionItemGroup = { label: `Labels${limitInfo}`, items: newItems };
+ const newSuggestion: CompletionItemGroup = {
+ label: `Labels${limitInfo}`,
+ items: newItems,
+ searchFunctionType: SearchFunctionType.Fuzzy,
+ };
suggestions.push(newSuggestion);
}
}
diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts
index f5b4f32100e..1400466551d 100644
--- a/public/app/plugins/datasource/prometheus/promql.ts
+++ b/public/app/plugins/datasource/prometheus/promql.ts
@@ -4,14 +4,14 @@ import { CompletionItem } from '@grafana/ui';
// When changing RATE_RANGES, check if Loki/LogQL ranges should be changed too
// @see public/app/plugins/datasource/loki/language_provider.ts
export const RATE_RANGES: CompletionItem[] = [
- { label: '$__interval', sortText: '$__interval' },
- { label: '$__rate_interval', sortText: '$__rate_interval' },
- { label: '1m', sortText: '00:01:00' },
- { label: '5m', sortText: '00:05:00' },
- { label: '10m', sortText: '00:10:00' },
- { label: '30m', sortText: '00:30:00' },
- { label: '1h', sortText: '01:00:00' },
- { label: '1d', sortText: '24:00:00' },
+ { label: '$__interval', sortValue: '$__interval' },
+ { label: '$__rate_interval', sortValue: '$__rate_interval' },
+ { label: '1m', sortValue: '00:01:00' },
+ { label: '5m', sortValue: '00:05:00' },
+ { label: '10m', sortValue: '00:10:00' },
+ { label: '30m', sortValue: '00:30:00' },
+ { label: '1h', sortValue: '01:00:00' },
+ { label: '1d', sortValue: '24:00:00' },
];
export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without'];