Commit 239dfbc9 by David Committed by GitHub

Merge pull request #13824 from grafana/davkal/explore-plugins

Explore: move suggestions logic to datasource language provider
parents 361864be 6f2315d5
...@@ -695,11 +695,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -695,11 +695,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
}); });
} }
request = url => {
const { datasource } = this.state;
return datasource.metadataRequest(url);
};
cloneState(): ExploreState { cloneState(): ExploreState {
// Copy state, but copy queries including modifications // Copy state, but copy queries including modifications
return { return {
...@@ -831,9 +826,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -831,9 +826,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
{datasource && !datasourceError ? ( {datasource && !datasourceError ? (
<div className="explore-container"> <div className="explore-container">
<QueryRows <QueryRows
datasource={datasource}
history={history} history={history}
queries={queries} queries={queries}
request={this.request}
onAddQueryRow={this.onAddQueryRow} onAddQueryRow={this.onAddQueryRow}
onChangeQuery={this.onChangeQuery} onChangeQuery={this.onChangeQuery}
onClickHintFix={this.onModifyQueries} onClickHintFix={this.onModifyQueries}
......
import React from 'react'; import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Plain from 'slate-plain-serializer';
import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
Enzyme.configure({ adapter: new Adapter() });
describe('PromQueryField typeahead handling', () => {
const defaultProps = {
request: () => ({ data: { data: [] } }),
};
it('returns default suggestions on emtpty context', () => {
const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
const result = instance.getTypeahead({ text: '', prefix: '', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
describe('range suggestions', () => {
it('returns range suggestions in range context', () => {
const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
const result = instance.getTypeahead({ text: '1', prefix: '1', wrapperClasses: ['context-range'] });
expect(result.context).toBe('context-range');
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toEqual([
{
items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }],
label: 'Range vector',
},
]);
});
});
describe('metric suggestions', () => {
it('returns metrics suggestions by default', () => {
const instance = shallow(
<PromQueryField {...defaultProps} metrics={['foo', 'bar']} />
).instance() as PromQueryField;
const result = instance.getTypeahead({ text: 'a', prefix: 'a', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
it('returns default suggestions after a binary operator', () => {
const instance = shallow(
<PromQueryField {...defaultProps} metrics={['foo', 'bar']} />
).instance() as PromQueryField;
const result = instance.getTypeahead({ text: '*', prefix: '', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
});
describe('label suggestions', () => {
it('returns default label suggestions on label context and no metric', () => {
const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
const value = Plain.deserialize('{}');
const range = value.selection.merge({
anchorOffset: 1,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
});
it('returns label suggestions on label context and metric', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric"}': ['bar'] }} />
).instance() as PromQueryField;
const value = Plain.deserialize('metric{}');
const range = value.selection.merge({
anchorOffset: 7,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions on label context but leaves out labels that already exist', () => {
const instance = shallow(
<PromQueryField
{...defaultProps}
labelKeys={{ '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }}
/>
).instance() as PromQueryField;
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
const range = value.selection.merge({
anchorOffset: 36,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label value suggestions inside a label value context after a negated matching operator', () => {
const instance = shallow(
<PromQueryField
{...defaultProps}
labelKeys={{ '{}': ['label'] }}
labelValues={{ '{}': { label: ['a', 'b', 'c'] } }}
/>
).instance() as PromQueryField;
const value = Plain.deserialize('{label!=}');
const range = value.selection.merge({ anchorOffset: 8 });
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '!=',
prefix: '',
wrapperClasses: ['context-labels'],
labelKey: 'label',
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([
{
items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
label: 'Label values for "label"',
},
]);
});
it('returns a refresher on label context and unavailable metric', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
).instance() as PromQueryField;
const value = Plain.deserialize('metric{}');
const range = value.selection.merge({
anchorOffset: 7,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBeUndefined();
expect(result.refresher).toBeInstanceOf(Promise);
expect(result.suggestions).toEqual([]);
});
it('returns label values on label context when given a metric and a label key', () => {
const instance = shallow(
<PromQueryField
{...defaultProps}
labelKeys={{ '{__name__="metric"}': ['bar'] }}
labelValues={{ '{__name__="metric"}': { bar: ['baz'] } }}
/>
).instance() as PromQueryField;
const value = Plain.deserialize('metric{bar=ba}');
const range = value.selection.merge({
anchorOffset: 13,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '=ba',
prefix: 'ba',
wrapperClasses: ['context-labels'],
labelKey: 'bar',
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
});
it('returns label suggestions on aggregation context and metric w/ selector', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric",foo="xx"}': ['bar'] }} />
).instance() as PromQueryField;
const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
const range = value.selection.merge({
anchorOffset: 26,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions on aggregation context and metric w/o selector', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric"}': ['bar'] }} />
).instance() as PromQueryField;
const value = Plain.deserialize('sum(metric) by ()');
const range = value.selection.merge({
anchorOffset: 16,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
});
});
describe('groupMetricsByPrefix()', () => { describe('groupMetricsByPrefix()', () => {
it('returns an empty group for no metrics', () => { it('returns an empty group for no metrics', () => {
......
...@@ -5,6 +5,8 @@ import { Change, Value } from 'slate'; ...@@ -5,6 +5,8 @@ import { Change, Value } from 'slate';
import { Editor } from 'slate-react'; import { Editor } from 'slate-react';
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
import ClearPlugin from './slate-plugins/clear'; import ClearPlugin from './slate-plugins/clear';
import NewlinePlugin from './slate-plugins/newline'; import NewlinePlugin from './slate-plugins/newline';
...@@ -13,87 +15,17 @@ import { makeFragment, makeValue } from './Value'; ...@@ -13,87 +15,17 @@ import { makeFragment, makeValue } from './Value';
export const TYPEAHEAD_DEBOUNCE = 100; export const TYPEAHEAD_DEBOUNCE = 100;
function getSuggestionByIndex(suggestions: SuggestionGroup[], index: number): Suggestion { function getSuggestionByIndex(suggestions: CompletionItemGroup[], index: number): CompletionItem {
// Flatten suggestion groups // Flatten suggestion groups
const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []); const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []);
const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length; const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length;
return flattenedSuggestions[correctedIndex]; return flattenedSuggestions[correctedIndex];
} }
function hasSuggestions(suggestions: SuggestionGroup[]): boolean { function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
return suggestions && suggestions.length > 0; return suggestions && suggestions.length > 0;
} }
export interface Suggestion {
/**
* The label of this completion item. By default
* this is also the text that is inserted when selecting
* this completion.
*/
label: string;
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
kind?: string;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*/
detail?: string;
/**
* A human-readable string, can be Markdown, that represents a doc-comment.
*/
documentation?: string;
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the `label` is used.
*/
sortText?: string;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the `label` is used.
*/
filterText?: string;
/**
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the `label` is used.
*/
insertText?: string;
/**
* Delete number of characters before the caret position,
* by default the letters from the beginning of the word.
*/
deleteBackwards?: number;
/**
* Number of steps to move after the insertion, can be negative.
*/
move?: number;
}
export interface SuggestionGroup {
/**
* Label that will be displayed for all entries of this group.
*/
label: string;
/**
* List of suggestions of this group.
*/
items: Suggestion[];
/**
* If true, match only by prefix (and not mid-word).
*/
prefixMatch?: boolean;
/**
* If true, do not filter items in this group based on the search.
*/
skipFilter?: boolean;
/**
* If true, do not sort items.
*/
skipSort?: boolean;
}
interface TypeaheadFieldProps { interface TypeaheadFieldProps {
additionalPlugins?: any[]; additionalPlugins?: any[];
cleanText?: (text: string) => string; cleanText?: (text: string) => string;
...@@ -110,7 +42,7 @@ interface TypeaheadFieldProps { ...@@ -110,7 +42,7 @@ interface TypeaheadFieldProps {
} }
export interface TypeaheadFieldState { export interface TypeaheadFieldState {
suggestions: SuggestionGroup[]; suggestions: CompletionItemGroup[];
typeaheadContext: string | null; typeaheadContext: string | null;
typeaheadIndex: number; typeaheadIndex: number;
typeaheadPrefix: string; typeaheadPrefix: string;
...@@ -127,12 +59,6 @@ export interface TypeaheadInput { ...@@ -127,12 +59,6 @@ export interface TypeaheadInput {
wrapperNode: Element; wrapperNode: Element;
} }
export interface TypeaheadOutput {
context?: string;
refresher?: Promise<{}>;
suggestions: SuggestionGroup[];
}
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> { class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
menuEl: HTMLElement | null; menuEl: HTMLElement | null;
plugins: any[]; plugins: any[];
...@@ -293,7 +219,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField ...@@ -293,7 +219,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
} }
}, TYPEAHEAD_DEBOUNCE); }, TYPEAHEAD_DEBOUNCE);
applyTypeahead(change: Change, suggestion: Suggestion): Change { applyTypeahead(change: Change, suggestion: CompletionItem): Change {
const { cleanText, onWillApplySuggestion, syntax } = this.props; const { cleanText, onWillApplySuggestion, syntax } = this.props;
const { typeaheadPrefix, typeaheadText } = this.state; const { typeaheadPrefix, typeaheadText } = this.state;
let suggestionText = suggestion.insertText || suggestion.label; let suggestionText = suggestion.insertText || suggestion.label;
...@@ -422,7 +348,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField ...@@ -422,7 +348,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
} }
}; };
onClickMenu = (item: Suggestion) => { onClickMenu = (item: CompletionItem) => {
// Manually triggering change // Manually triggering change
const change = this.applyTypeahead(this.state.value.change(), item); const change = this.applyTypeahead(this.state.value.change(), item);
this.onChange(change); this.onChange(change);
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { QueryTransaction } from 'app/types/explore'; import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore';
// TODO make this datasource-plugin-dependent // TODO make this datasource-plugin-dependent
import QueryField from './PromQueryField'; import QueryField from './PromQueryField';
import QueryTransactions from './QueryTransactions'; import QueryTransactions from './QueryTransactions';
function getFirstHintFromTransactions(transactions: QueryTransaction[]) { function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
if (transaction) { if (transaction) {
return transaction.hints[0]; return transaction.hints[0];
...@@ -14,7 +14,30 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]) { ...@@ -14,7 +14,30 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]) {
return undefined; return undefined;
} }
class QueryRow extends PureComponent<any, {}> { interface QueryRowEventHandlers {
onAddQueryRow: (index: number) => void;
onChangeQuery: (value: string, index: number, override?: boolean) => void;
onClickHintFix: (action: object, index?: number) => void;
onExecuteQuery: () => void;
onRemoveQueryRow: (index: number) => void;
}
interface QueryRowCommonProps {
className?: string;
datasource: any;
history: HistoryItem[];
// Temporarily
supportsLogs?: boolean;
transactions: QueryTransaction[];
}
type QueryRowProps = QueryRowCommonProps &
QueryRowEventHandlers & {
index: number;
query: string;
};
class QueryRow extends PureComponent<QueryRowProps> {
onChangeQuery = (value, override?: boolean) => { onChangeQuery = (value, override?: boolean) => {
const { index, onChangeQuery } = this.props; const { index, onChangeQuery } = this.props;
if (onChangeQuery) { if (onChangeQuery) {
...@@ -55,8 +78,8 @@ class QueryRow extends PureComponent<any, {}> { ...@@ -55,8 +78,8 @@ class QueryRow extends PureComponent<any, {}> {
}; };
render() { render() {
const { history, query, request, supportsLogs, transactions } = this.props; const { datasource, history, query, supportsLogs, transactions } = this.props;
const transactionWithError = transactions.find(t => t.error); const transactionWithError = transactions.find(t => t.error !== undefined);
const hint = getFirstHintFromTransactions(transactions); const hint = getFirstHintFromTransactions(transactions);
const queryError = transactionWithError ? transactionWithError.error : null; const queryError = transactionWithError ? transactionWithError.error : null;
return ( return (
...@@ -66,6 +89,7 @@ class QueryRow extends PureComponent<any, {}> { ...@@ -66,6 +89,7 @@ class QueryRow extends PureComponent<any, {}> {
</div> </div>
<div className="query-row-field"> <div className="query-row-field">
<QueryField <QueryField
datasource={datasource}
error={queryError} error={queryError}
hint={hint} hint={hint}
initialQuery={query} initialQuery={query}
...@@ -73,7 +97,6 @@ class QueryRow extends PureComponent<any, {}> { ...@@ -73,7 +97,6 @@ class QueryRow extends PureComponent<any, {}> {
onClickHintFix={this.onClickHintFix} onClickHintFix={this.onClickHintFix}
onPressEnter={this.onPressEnter} onPressEnter={this.onPressEnter}
onQueryChange={this.onChangeQuery} onQueryChange={this.onChangeQuery}
request={request}
supportsLogs={supportsLogs} supportsLogs={supportsLogs}
/> />
</div> </div>
...@@ -93,9 +116,14 @@ class QueryRow extends PureComponent<any, {}> { ...@@ -93,9 +116,14 @@ class QueryRow extends PureComponent<any, {}> {
} }
} }
export default class QueryRows extends PureComponent<any, {}> { type QueryRowsProps = QueryRowCommonProps &
QueryRowEventHandlers & {
queries: Query[];
};
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() { render() {
const { className = '', queries, queryHints, transactions, ...handlers } = this.props; const { className = '', queries, transactions, ...handlers } = this.props;
return ( return (
<div className={className}> <div className={className}>
{queries.map((q, index) => ( {queries.map((q, index) => (
......
import React from 'react'; import React from 'react';
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import { Suggestion, SuggestionGroup } from './QueryField'; import { CompletionItem, CompletionItemGroup } from 'app/types/explore';
function scrollIntoView(el: HTMLElement) { function scrollIntoView(el: HTMLElement) {
if (!el || !el.offsetParent) { if (!el || !el.offsetParent) {
...@@ -15,12 +15,12 @@ function scrollIntoView(el: HTMLElement) { ...@@ -15,12 +15,12 @@ function scrollIntoView(el: HTMLElement) {
interface TypeaheadItemProps { interface TypeaheadItemProps {
isSelected: boolean; isSelected: boolean;
item: Suggestion; item: CompletionItem;
onClickItem: (Suggestion) => void; onClickItem: (Suggestion) => void;
prefix?: string; prefix?: string;
} }
class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> { class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
el: HTMLElement; el: HTMLElement;
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
...@@ -53,14 +53,14 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> { ...@@ -53,14 +53,14 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
} }
interface TypeaheadGroupProps { interface TypeaheadGroupProps {
items: Suggestion[]; items: CompletionItem[];
label: string; label: string;
onClickItem: (Suggestion) => void; onClickItem: (CompletionItem) => void;
selected: Suggestion; selected: CompletionItem;
prefix?: string; prefix?: string;
} }
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> { class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps> {
render() { render() {
const { items, label, selected, onClickItem, prefix } = this.props; const { items, label, selected, onClickItem, prefix } = this.props;
return ( return (
...@@ -85,13 +85,13 @@ class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> { ...@@ -85,13 +85,13 @@ class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
} }
interface TypeaheadProps { interface TypeaheadProps {
groupedItems: SuggestionGroup[]; groupedItems: CompletionItemGroup[];
menuRef: any; menuRef: any;
selectedItem: Suggestion | null; selectedItem: CompletionItem | null;
onClickItem: (Suggestion) => void; onClickItem: (Suggestion) => void;
prefix?: string; prefix?: string;
} }
class Typeahead extends React.PureComponent<TypeaheadProps, {}> { class Typeahead extends React.PureComponent<TypeaheadProps> {
render() { render() {
const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props; const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props;
return ( return (
......
...@@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn'; ...@@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
import PrometheusMetricFindQuery from './metric_find_query'; import PrometheusMetricFindQuery from './metric_find_query';
import { ResultTransformer } from './result_transformer'; import { ResultTransformer } from './result_transformer';
import PrometheusLanguageProvider from './language_provider';
import { BackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv } from 'app/core/services/backend_srv';
import addLabelToQuery from './add_label_to_query'; import addLabelToQuery from './add_label_to_query';
...@@ -60,6 +61,7 @@ export class PrometheusDatasource { ...@@ -60,6 +61,7 @@ export class PrometheusDatasource {
interval: string; interval: string;
queryTimeout: string; queryTimeout: string;
httpMethod: string; httpMethod: string;
languageProvider: PrometheusLanguageProvider;
resultTransformer: ResultTransformer; resultTransformer: ResultTransformer;
/** @ngInject */ /** @ngInject */
...@@ -76,6 +78,7 @@ export class PrometheusDatasource { ...@@ -76,6 +78,7 @@ export class PrometheusDatasource {
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
this.resultTransformer = new ResultTransformer(templateSrv); this.resultTransformer = new ResultTransformer(templateSrv);
this.ruleMappings = {}; this.ruleMappings = {};
this.languageProvider = new PrometheusLanguageProvider(this);
} }
init() { init() {
......
...@@ -23,9 +23,6 @@ export function processLabels(labels, withName = false) { ...@@ -23,9 +23,6 @@ export function processLabels(labels, withName = false) {
return { values, keys: Object.keys(values) }; return { values, keys: Object.keys(values) };
} }
// Strip syntax chars
export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
const selectorRegexp = /\{[^}]*?\}/; const selectorRegexp = /\{[^}]*?\}/;
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
......
import _ from 'lodash'; import _ from 'lodash';
export function getQueryHints(query: string, series?: any[], datasource?: any): any[] { import { QueryHint } from 'app/types/explore';
export function getQueryHints(query: string, series?: any[], datasource?: any): QueryHint[] {
const hints = []; const hints = [];
// ..._bucket metric needs a histogram_quantile() // ..._bucket metric needs a histogram_quantile()
......
import Plain from 'slate-plain-serializer';
import LanguageProvider from '../language_provider';
describe('Language completion provider', () => {
const datasource = {
metadataRequest: () => ({ data: { data: [] } }),
};
it('returns default suggestions on emtpty context', () => {
const instance = new LanguageProvider(datasource);
const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
describe('range suggestions', () => {
it('returns range suggestions in range context', () => {
const instance = new LanguageProvider(datasource);
const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] });
expect(result.context).toBe('context-range');
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toEqual([
{
items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }],
label: 'Range vector',
},
]);
});
});
describe('metric suggestions', () => {
it('returns metrics suggestions by default', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
it('returns default suggestions after a binary operator', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
});
});
describe('label suggestions', () => {
it('returns default label suggestions on label context and no metric', () => {
const instance = new LanguageProvider(datasource);
const value = Plain.deserialize('{}');
const range = value.selection.merge({
anchorOffset: 1,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
});
it('returns label suggestions on label context and metric', () => {
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
const value = Plain.deserialize('metric{}');
const range = value.selection.merge({
anchorOffset: 7,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions on label context but leaves out labels that already exist', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] },
});
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
const range = value.selection.merge({
anchorOffset: 36,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label value suggestions inside a label value context after a negated matching operator', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{}': ['label'] },
labelValues: { '{}': { label: ['a', 'b', 'c'] } },
});
const value = Plain.deserialize('{label!=}');
const range = value.selection.merge({ anchorOffset: 8 });
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '!=',
prefix: '',
wrapperClasses: ['context-labels'],
labelKey: 'label',
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([
{
items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
label: 'Label values for "label"',
},
]);
});
it('returns a refresher on label context and unavailable metric', () => {
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } });
const value = Plain.deserialize('metric{}');
const range = value.selection.merge({
anchorOffset: 7,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBeUndefined();
expect(result.refresher).toBeInstanceOf(Promise);
expect(result.suggestions).toEqual([]);
});
it('returns label values on label context when given a metric and a label key', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric"}': ['bar'] },
labelValues: { '{__name__="metric"}': { bar: ['baz'] } },
});
const value = Plain.deserialize('metric{bar=ba}');
const range = value.selection.merge({
anchorOffset: 13,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '=ba',
prefix: 'ba',
wrapperClasses: ['context-labels'],
labelKey: 'bar',
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
});
it('returns label suggestions on aggregation context and metric w/ selector', () => {
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } });
const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
const range = value.selection.merge({
anchorOffset: 26,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions on aggregation context and metric w/o selector', () => {
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
const value = Plain.deserialize('sum(metric) by ()');
const range = value.selection.merge({
anchorOffset: 16,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
});
});
import { parseSelector } from './prometheus'; import { parseSelector } from '../language_utils';
describe('parseSelector()', () => { describe('parseSelector()', () => {
let parsed; let parsed;
......
import { Value } from 'slate';
export interface CompletionItem {
/**
* The label of this completion item. By default
* this is also the text that is inserted when selecting
* this completion.
*/
label: string;
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
kind?: string;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*/
detail?: string;
/**
* A human-readable string, can be Markdown, that represents a doc-comment.
*/
documentation?: string;
/**
* A string that should be used when comparing this item
* with other items. When `falsy` the `label` is used.
*/
sortText?: string;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the `label` is used.
*/
filterText?: string;
/**
* A string or snippet that should be inserted in a document when selecting
* this completion. When `falsy` the `label` is used.
*/
insertText?: string;
/**
* Delete number of characters before the caret position,
* by default the letters from the beginning of the word.
*/
deleteBackwards?: number;
/**
* Number of steps to move after the insertion, can be negative.
*/
move?: number;
}
export interface CompletionItemGroup {
/**
* Label that will be displayed for all entries of this group.
*/
label: string;
/**
* List of suggestions of this group.
*/
items: CompletionItem[];
/**
* If true, match only by prefix (and not mid-word).
*/
prefixMatch?: boolean;
/**
* If true, do not filter items in this group based on the search.
*/
skipFilter?: boolean;
/**
* If true, do not sort items.
*/
skipSort?: boolean;
}
interface ExploreDatasource { interface ExploreDatasource {
value: string; value: string;
label: string; label: string;
...@@ -8,6 +80,26 @@ export interface HistoryItem { ...@@ -8,6 +80,26 @@ export interface HistoryItem {
query: string; query: string;
} }
export abstract class LanguageProvider {
datasource: any;
request: (url) => Promise<any>;
start: () => Promise<any>;
}
export interface TypeaheadInput {
text: string;
prefix: string;
wrapperClasses: string[];
labelKey?: string;
value?: Value;
}
export interface TypeaheadOutput {
context?: string;
refresher?: Promise<{}>;
suggestions: CompletionItemGroup[];
}
export interface Range { export interface Range {
from: string; from: string;
to: string; to: string;
...@@ -18,11 +110,28 @@ export interface Query { ...@@ -18,11 +110,28 @@ export interface Query {
key?: string; key?: string;
} }
export interface QueryFix {
type: string;
label: string;
action?: QueryFixAction;
}
export interface QueryFixAction {
type: string;
query?: string;
}
export interface QueryHint {
type: string;
label: string;
fix?: QueryFix;
}
export interface QueryTransaction { export interface QueryTransaction {
id: string; id: string;
done: boolean; done: boolean;
error?: string; error?: string;
hints?: any[]; hints?: QueryHint[];
latency: number; latency: number;
options: any; options: any;
query: string; query: string;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment