mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13824 from grafana/davkal/explore-plugins
Explore: move suggestions logic to datasource language provider
This commit is contained in:
commit
239dfbc9ae
@ -695,11 +695,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
});
|
||||
}
|
||||
|
||||
request = url => {
|
||||
const { datasource } = this.state;
|
||||
return datasource.metadataRequest(url);
|
||||
};
|
||||
|
||||
cloneState(): ExploreState {
|
||||
// Copy state, but copy queries including modifications
|
||||
return {
|
||||
@ -831,9 +826,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
{datasource && !datasourceError ? (
|
||||
<div className="explore-container">
|
||||
<QueryRows
|
||||
datasource={datasource}
|
||||
history={history}
|
||||
queries={queries}
|
||||
request={this.request}
|
||||
onAddQueryRow={this.onAddQueryRow}
|
||||
onChangeQuery={this.onChangeQuery}
|
||||
onClickHintFix={this.onModifyQueries}
|
||||
|
@ -1,231 +1,4 @@
|
||||
import React from 'react';
|
||||
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' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
|
||||
|
||||
describe('groupMetricsByPrefix()', () => {
|
||||
it('returns an empty group for no metrics', () => {
|
||||
|
@ -1,67 +1,23 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { Value } from 'slate';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from './utils/dom';
|
||||
import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
|
||||
import BracesPlugin from './slate-plugins/braces';
|
||||
import RunnerPlugin from './slate-plugins/runner';
|
||||
import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus';
|
||||
|
||||
import TypeaheadField, {
|
||||
Suggestion,
|
||||
SuggestionGroup,
|
||||
TypeaheadInput,
|
||||
TypeaheadFieldState,
|
||||
TypeaheadOutput,
|
||||
} from './QueryField';
|
||||
import TypeaheadField, { TypeaheadInput, TypeaheadFieldState } from './QueryField';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const HISTOGRAM_SELECTOR = '{le!=""}'; // Returns all timeseries for histograms
|
||||
const HISTORY_ITEM_COUNT = 5;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
const METRIC_MARK = 'metric';
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
||||
|
||||
export const wrapLabel = (label: string) => ({ label });
|
||||
export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
|
||||
suggestion.move = -1;
|
||||
return suggestion;
|
||||
};
|
||||
|
||||
// Syntax highlighting
|
||||
Prism.languages[PRISM_SYNTAX] = PrismPromql;
|
||||
function setPrismTokens(language, field, values, alias = 'variable') {
|
||||
Prism.languages[language][field] = {
|
||||
alias,
|
||||
pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
|
||||
};
|
||||
}
|
||||
|
||||
export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion {
|
||||
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||
const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label);
|
||||
const count = historyForItem.length;
|
||||
const recent = historyForItem[0];
|
||||
let hint = `Queried ${count} times in the last 24h.`;
|
||||
if (recent) {
|
||||
const lastQueried = moment(recent.ts).fromNow();
|
||||
hint = `${hint} Last queried ${lastQueried}.`;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
documentation: hint,
|
||||
};
|
||||
}
|
||||
|
||||
export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
|
||||
// Filter out recording rules and insert as first option
|
||||
const ruleRegex = /:\w+:/;
|
||||
@ -133,48 +89,36 @@ interface CascaderOption {
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string;
|
||||
hint?: any;
|
||||
histogramMetrics?: string[];
|
||||
history?: any[];
|
||||
initialQuery?: string | null;
|
||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
metrics?: string[];
|
||||
metricsByPrefix?: CascaderOption[];
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: string, override?: boolean) => void;
|
||||
portalOrigin?: string;
|
||||
request?: (url: string) => any;
|
||||
supportsLogs?: boolean; // To be removed after Logging gets its own query field
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
histogramMetrics: string[];
|
||||
labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
logLabelOptions: any[];
|
||||
metrics: string[];
|
||||
metricsOptions: any[];
|
||||
metricsByPrefix: CascaderOption[];
|
||||
syntaxLoaded: boolean;
|
||||
}
|
||||
|
||||
interface PromTypeaheadInput {
|
||||
text: string;
|
||||
prefix: string;
|
||||
wrapperClasses: string[];
|
||||
labelKey?: string;
|
||||
value?: Value;
|
||||
}
|
||||
|
||||
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
||||
plugins: any[];
|
||||
languageProvider: any;
|
||||
|
||||
constructor(props: PromQueryFieldProps, context) {
|
||||
super(props, context);
|
||||
|
||||
if (props.datasource.languageProvider) {
|
||||
this.languageProvider = props.datasource.languageProvider;
|
||||
}
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
RunnerPlugin({ handler: props.onPressEnter }),
|
||||
@ -185,26 +129,16 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
];
|
||||
|
||||
this.state = {
|
||||
histogramMetrics: props.histogramMetrics || [],
|
||||
labelKeys: props.labelKeys || {},
|
||||
labelValues: props.labelValues || {},
|
||||
logLabelOptions: [],
|
||||
metrics: props.metrics || [],
|
||||
metricsByPrefix: props.metricsByPrefix || [],
|
||||
metricsByPrefix: [],
|
||||
metricsOptions: [],
|
||||
syntaxLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Temporarily reused by logging
|
||||
const { supportsLogs } = this.props;
|
||||
if (supportsLogs) {
|
||||
this.fetchLogLabels();
|
||||
} else {
|
||||
// Usual actions
|
||||
this.fetchMetricNames();
|
||||
this.fetchHistogramMetrics();
|
||||
if (this.languageProvider) {
|
||||
this.languageProvider.start().then(() => this.onReceiveMetrics());
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,15 +196,19 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
};
|
||||
|
||||
onReceiveMetrics = () => {
|
||||
const { histogramMetrics, metrics, metricsByPrefix } = this.state;
|
||||
const { histogramMetrics, metrics } = this.languageProvider;
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update global prism config
|
||||
setPrismTokens(PRISM_SYNTAX, METRIC_MARK, metrics);
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
Prism.languages[PRISM_SYNTAX][METRIC_MARK] = {
|
||||
alias: 'variable',
|
||||
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
|
||||
};
|
||||
|
||||
// Build metrics tree
|
||||
const metricsByPrefix = groupMetricsByPrefix(metrics);
|
||||
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
||||
const metricsOptions = [
|
||||
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
|
||||
@ -281,6 +219,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
};
|
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
||||
if (!this.languageProvider) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const { prefix, text, value, wrapperNode } = typeahead;
|
||||
|
||||
// Get DOM-dependent context
|
||||
@ -289,279 +232,20 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
const labelKey = labelKeyNode && labelKeyNode.textContent;
|
||||
const nextChar = getNextCharacter();
|
||||
|
||||
const result = this.getTypeahead({ text, value, prefix, wrapperClasses, labelKey });
|
||||
const result = this.languageProvider.provideCompletionItems(
|
||||
{ text, value, prefix, wrapperClasses, labelKey },
|
||||
{ history }
|
||||
);
|
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
getTypeahead({ prefix, wrapperClasses, text }: PromTypeaheadInput): TypeaheadOutput {
|
||||
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||
const tokenRecognized = wrapperClasses.length > 3;
|
||||
// Determine candidates by CSS context
|
||||
if (_.includes(wrapperClasses, 'context-range')) {
|
||||
// Suggestions for metric[|]
|
||||
return this.getRangeTypeahead();
|
||||
} else if (_.includes(wrapperClasses, 'context-labels')) {
|
||||
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
|
||||
return this.getLabelTypeahead.apply(this, arguments);
|
||||
} else if (_.includes(wrapperClasses, 'context-aggregation')) {
|
||||
return this.getAggregationTypeahead.apply(this, arguments);
|
||||
} else if (
|
||||
// Show default suggestions in a couple of scenarios
|
||||
(prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token
|
||||
(prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace
|
||||
text.match(/[+\-*/^%]/) // Anything after binary operator
|
||||
) {
|
||||
return this.getEmptyTypeahead();
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: [],
|
||||
};
|
||||
}
|
||||
|
||||
getEmptyTypeahead(): TypeaheadOutput {
|
||||
const { history } = this.props;
|
||||
const { metrics } = this.state;
|
||||
const suggestions: SuggestionGroup[] = [];
|
||||
|
||||
if (history && history.length > 0) {
|
||||
const historyItems = _.chain(history)
|
||||
.uniqBy('query')
|
||||
.take(HISTORY_ITEM_COUNT)
|
||||
.map(h => h.query)
|
||||
.map(wrapLabel)
|
||||
.map(item => addHistoryMetadata(item, history))
|
||||
.value();
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
skipSort: true,
|
||||
label: 'History',
|
||||
items: historyItems,
|
||||
});
|
||||
}
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
label: 'Functions',
|
||||
items: FUNCTIONS.map(setFunctionMove),
|
||||
});
|
||||
|
||||
if (metrics) {
|
||||
suggestions.push({
|
||||
label: 'Metrics',
|
||||
items: metrics.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
getRangeTypeahead(): TypeaheadOutput {
|
||||
return {
|
||||
context: 'context-range',
|
||||
suggestions: [
|
||||
{
|
||||
label: 'Range vector',
|
||||
items: [...RATE_RANGES].map(wrapLabel),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
getAggregationTypeahead({ value }: PromTypeaheadInput): TypeaheadOutput {
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: SuggestionGroup[] = [];
|
||||
|
||||
// sum(foo{bar="1"}) by (|)
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
// sum(foo{bar="1"}) by (
|
||||
const leftSide = line.slice(0, cursorOffset);
|
||||
const openParensAggregationIndex = leftSide.lastIndexOf('(');
|
||||
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
|
||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
||||
// foo{bar="1"}
|
||||
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
|
||||
|
||||
const labelKeys = this.state.labelKeys[selector];
|
||||
if (labelKeys) {
|
||||
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector);
|
||||
}
|
||||
|
||||
return {
|
||||
refresher,
|
||||
suggestions,
|
||||
context: 'context-aggregation',
|
||||
};
|
||||
}
|
||||
|
||||
getLabelTypeahead({ text, wrapperClasses, labelKey, value }: PromTypeaheadInput): TypeaheadOutput {
|
||||
let context: string;
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: SuggestionGroup[] = [];
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
|
||||
// Get normalized selector
|
||||
let selector;
|
||||
let parsedSelector;
|
||||
try {
|
||||
parsedSelector = parseSelector(line, cursorOffset);
|
||||
selector = parsedSelector.selector;
|
||||
} catch {
|
||||
selector = EMPTY_SELECTOR;
|
||||
}
|
||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||
|
||||
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
|
||||
// Label values
|
||||
if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
|
||||
const labelValues = this.state.labelValues[selector][labelKey];
|
||||
context = 'context-label-values';
|
||||
suggestions.push({
|
||||
label: `Label values for "${labelKey}"`,
|
||||
items: labelValues.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Label keys
|
||||
const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
||||
if (labelKeys) {
|
||||
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||
if (possibleKeys.length > 0) {
|
||||
context = 'context-labels';
|
||||
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Query labels for selector
|
||||
// Temporarily add skip for logging
|
||||
if (selector && !this.state.labelValues[selector] && !this.props.supportsLogs) {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
// Query label values for default labels
|
||||
refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector, !containsMetric);
|
||||
}
|
||||
}
|
||||
|
||||
return { context, refresher, suggestions };
|
||||
}
|
||||
|
||||
request = url => {
|
||||
if (this.props.request) {
|
||||
return this.props.request(url);
|
||||
}
|
||||
return fetch(url);
|
||||
};
|
||||
|
||||
fetchHistogramMetrics() {
|
||||
this.fetchSeriesLabels(HISTOGRAM_SELECTOR, true, () => {
|
||||
const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR];
|
||||
if (histogramSeries && histogramSeries['__name__']) {
|
||||
const histogramMetrics = histogramSeries['__name__'].slice().sort();
|
||||
this.setState({ histogramMetrics }, this.onReceiveMetrics);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Temporarily here while reusing this field for logging
|
||||
async fetchLogLabels() {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const labelKeys = body.data.slice().sort();
|
||||
const labelKeysBySelector = {
|
||||
...this.state.labelKeys,
|
||||
[EMPTY_SELECTOR]: labelKeys,
|
||||
};
|
||||
const labelValuesByKey = {};
|
||||
const logLabelOptions = [];
|
||||
for (const key of labelKeys) {
|
||||
const valuesUrl = `/api/prom/label/${key}/values`;
|
||||
const res = await this.request(valuesUrl);
|
||||
const body = await (res.data || res.json());
|
||||
const values = body.data.slice().sort();
|
||||
labelValuesByKey[key] = values;
|
||||
logLabelOptions.push({
|
||||
label: key,
|
||||
value: key,
|
||||
children: values.map(value => ({ label: value, value })),
|
||||
});
|
||||
}
|
||||
const labelValues = { [EMPTY_SELECTOR]: labelValuesByKey };
|
||||
this.setState({ labelKeys: labelKeysBySelector, labelValues, logLabelOptions });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLabelValues(key: string) {
|
||||
const url = `/api/v1/label/${key}/values`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const exisingValues = this.state.labelValues[EMPTY_SELECTOR];
|
||||
const values = {
|
||||
...exisingValues,
|
||||
[key]: body.data,
|
||||
};
|
||||
const labelValues = {
|
||||
...this.state.labelValues,
|
||||
[EMPTY_SELECTOR]: values,
|
||||
};
|
||||
this.setState({ labelValues });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSeriesLabels(name: string, withName?: boolean, callback?: () => void) {
|
||||
const url = `/api/v1/series?match[]=${name}`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const { keys, values } = processLabels(body.data, withName);
|
||||
const labelKeys = {
|
||||
...this.state.labelKeys,
|
||||
[name]: keys,
|
||||
};
|
||||
const labelValues = {
|
||||
...this.state.labelValues,
|
||||
[name]: values,
|
||||
};
|
||||
this.setState({ labelKeys, labelValues }, callback);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMetricNames() {
|
||||
const url = '/api/v1/label/__name__/values';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const metrics = body.data;
|
||||
const metricsByPrefix = groupMetricsByPrefix(metrics);
|
||||
this.setState({ metrics, metricsByPrefix }, this.onReceiveMetrics);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery, supportsLogs } = this.props;
|
||||
const { logLabelOptions, metricsOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
|
||||
return (
|
||||
<div className="prom-query-field">
|
||||
|
@ -5,6 +5,8 @@ import { Change, Value } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
|
||||
import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
import ClearPlugin from './slate-plugins/clear';
|
||||
import NewlinePlugin from './slate-plugins/newline';
|
||||
|
||||
@ -13,87 +15,17 @@ import { makeFragment, makeValue } from './Value';
|
||||
|
||||
export const TYPEAHEAD_DEBOUNCE = 100;
|
||||
|
||||
function getSuggestionByIndex(suggestions: SuggestionGroup[], index: number): Suggestion {
|
||||
function getSuggestionByIndex(suggestions: CompletionItemGroup[], index: number): CompletionItem {
|
||||
// Flatten suggestion groups
|
||||
const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []);
|
||||
const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length;
|
||||
return flattenedSuggestions[correctedIndex];
|
||||
}
|
||||
|
||||
function hasSuggestions(suggestions: SuggestionGroup[]): boolean {
|
||||
function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
|
||||
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 {
|
||||
additionalPlugins?: any[];
|
||||
cleanText?: (text: string) => string;
|
||||
@ -110,7 +42,7 @@ interface TypeaheadFieldProps {
|
||||
}
|
||||
|
||||
export interface TypeaheadFieldState {
|
||||
suggestions: SuggestionGroup[];
|
||||
suggestions: CompletionItemGroup[];
|
||||
typeaheadContext: string | null;
|
||||
typeaheadIndex: number;
|
||||
typeaheadPrefix: string;
|
||||
@ -127,12 +59,6 @@ export interface TypeaheadInput {
|
||||
wrapperNode: Element;
|
||||
}
|
||||
|
||||
export interface TypeaheadOutput {
|
||||
context?: string;
|
||||
refresher?: Promise<{}>;
|
||||
suggestions: SuggestionGroup[];
|
||||
}
|
||||
|
||||
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
|
||||
menuEl: HTMLElement | null;
|
||||
plugins: any[];
|
||||
@ -293,7 +219,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
}
|
||||
}, TYPEAHEAD_DEBOUNCE);
|
||||
|
||||
applyTypeahead(change: Change, suggestion: Suggestion): Change {
|
||||
applyTypeahead(change: Change, suggestion: CompletionItem): Change {
|
||||
const { cleanText, onWillApplySuggestion, syntax } = this.props;
|
||||
const { typeaheadPrefix, typeaheadText } = this.state;
|
||||
let suggestionText = suggestion.insertText || suggestion.label;
|
||||
@ -422,7 +348,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
}
|
||||
};
|
||||
|
||||
onClickMenu = (item: Suggestion) => {
|
||||
onClickMenu = (item: CompletionItem) => {
|
||||
// Manually triggering change
|
||||
const change = this.applyTypeahead(this.state.value.change(), item);
|
||||
this.onChange(change);
|
||||
|
@ -1,12 +1,12 @@
|
||||
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
|
||||
import QueryField from './PromQueryField';
|
||||
import QueryTransactions from './QueryTransactions';
|
||||
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]) {
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||
if (transaction) {
|
||||
return transaction.hints[0];
|
||||
@ -14,7 +14,30 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]) {
|
||||
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) => {
|
||||
const { index, onChangeQuery } = this.props;
|
||||
if (onChangeQuery) {
|
||||
@ -55,8 +78,8 @@ class QueryRow extends PureComponent<any, {}> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { history, query, request, supportsLogs, transactions } = this.props;
|
||||
const transactionWithError = transactions.find(t => t.error);
|
||||
const { datasource, history, query, supportsLogs, transactions } = this.props;
|
||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||
const hint = getFirstHintFromTransactions(transactions);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
return (
|
||||
@ -66,6 +89,7 @@ class QueryRow extends PureComponent<any, {}> {
|
||||
</div>
|
||||
<div className="query-row-field">
|
||||
<QueryField
|
||||
datasource={datasource}
|
||||
error={queryError}
|
||||
hint={hint}
|
||||
initialQuery={query}
|
||||
@ -73,7 +97,6 @@ class QueryRow extends PureComponent<any, {}> {
|
||||
onClickHintFix={this.onClickHintFix}
|
||||
onPressEnter={this.onPressEnter}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
request={request}
|
||||
supportsLogs={supportsLogs}
|
||||
/>
|
||||
</div>
|
||||
@ -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() {
|
||||
const { className = '', queries, queryHints, transactions, ...handlers } = this.props;
|
||||
const { className = '', queries, transactions, ...handlers } = this.props;
|
||||
return (
|
||||
<div className={className}>
|
||||
{queries.map((q, index) => (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
import { Suggestion, SuggestionGroup } from './QueryField';
|
||||
import { CompletionItem, CompletionItemGroup } from 'app/types/explore';
|
||||
|
||||
function scrollIntoView(el: HTMLElement) {
|
||||
if (!el || !el.offsetParent) {
|
||||
@ -15,12 +15,12 @@ function scrollIntoView(el: HTMLElement) {
|
||||
|
||||
interface TypeaheadItemProps {
|
||||
isSelected: boolean;
|
||||
item: Suggestion;
|
||||
item: CompletionItem;
|
||||
onClickItem: (Suggestion) => void;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
||||
class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
|
||||
el: HTMLElement;
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -53,14 +53,14 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
||||
}
|
||||
|
||||
interface TypeaheadGroupProps {
|
||||
items: Suggestion[];
|
||||
items: CompletionItem[];
|
||||
label: string;
|
||||
onClickItem: (Suggestion) => void;
|
||||
selected: Suggestion;
|
||||
onClickItem: (CompletionItem) => void;
|
||||
selected: CompletionItem;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
|
||||
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps> {
|
||||
render() {
|
||||
const { items, label, selected, onClickItem, prefix } = this.props;
|
||||
return (
|
||||
@ -85,13 +85,13 @@ class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
|
||||
}
|
||||
|
||||
interface TypeaheadProps {
|
||||
groupedItems: SuggestionGroup[];
|
||||
groupedItems: CompletionItemGroup[];
|
||||
menuRef: any;
|
||||
selectedItem: Suggestion | null;
|
||||
selectedItem: CompletionItem | null;
|
||||
onClickItem: (Suggestion) => void;
|
||||
prefix?: string;
|
||||
}
|
||||
class Typeahead extends React.PureComponent<TypeaheadProps, {}> {
|
||||
class Typeahead extends React.PureComponent<TypeaheadProps> {
|
||||
render() {
|
||||
const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props;
|
||||
return (
|
||||
|
@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import { ResultTransformer } from './result_transformer';
|
||||
import PrometheusLanguageProvider from './language_provider';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import addLabelToQuery from './add_label_to_query';
|
||||
@ -60,6 +61,7 @@ export class PrometheusDatasource {
|
||||
interval: string;
|
||||
queryTimeout: string;
|
||||
httpMethod: string;
|
||||
languageProvider: PrometheusLanguageProvider;
|
||||
resultTransformer: ResultTransformer;
|
||||
|
||||
/** @ngInject */
|
||||
@ -76,6 +78,7 @@ export class PrometheusDatasource {
|
||||
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
|
||||
this.resultTransformer = new ResultTransformer(templateSrv);
|
||||
this.ruleMappings = {};
|
||||
this.languageProvider = new PrometheusLanguageProvider(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
|
334
public/app/plugins/datasource/prometheus/language_provider.ts
Normal file
334
public/app/plugins/datasource/prometheus/language_provider.ts
Normal file
@ -0,0 +1,334 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
CompletionItem,
|
||||
CompletionItemGroup,
|
||||
LanguageProvider,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
} from 'app/types/explore';
|
||||
|
||||
import { parseSelector, processLabels, RATE_RANGES } from './language_utils';
|
||||
import PromqlSyntax, { FUNCTIONS } from './promql';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTOGRAM_SELECTOR = '{le!=""}'; // Returns all timeseries for histograms
|
||||
const HISTORY_ITEM_COUNT = 5;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
|
||||
const wrapLabel = (label: string) => ({ label });
|
||||
|
||||
const setFunctionMove = (suggestion: CompletionItem): CompletionItem => {
|
||||
suggestion.move = -1;
|
||||
return suggestion;
|
||||
};
|
||||
|
||||
export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem {
|
||||
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||
const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label);
|
||||
const count = historyForItem.length;
|
||||
const recent = historyForItem[0];
|
||||
let hint = `Queried ${count} times in the last 24h.`;
|
||||
if (recent) {
|
||||
const lastQueried = moment(recent.ts).fromNow();
|
||||
hint = `${hint} Last queried ${lastQueried}.`;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
documentation: hint,
|
||||
};
|
||||
}
|
||||
|
||||
export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
histogramMetrics?: string[];
|
||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
metrics?: string[];
|
||||
logLabelOptions: any[];
|
||||
supportsLogs?: boolean;
|
||||
started: boolean;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
super();
|
||||
|
||||
this.datasource = datasource;
|
||||
this.histogramMetrics = [];
|
||||
this.labelKeys = {};
|
||||
this.labelValues = {};
|
||||
this.metrics = [];
|
||||
this.supportsLogs = false;
|
||||
this.started = false;
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
// Strip syntax chars
|
||||
cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||
|
||||
getSyntax() {
|
||||
return PromqlSyntax;
|
||||
}
|
||||
|
||||
request = url => {
|
||||
return this.datasource.metadataRequest(url);
|
||||
};
|
||||
|
||||
start = () => {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
return Promise.all([this.fetchMetricNames(), this.fetchHistogramMetrics()]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||
const tokenRecognized = wrapperClasses.length > 3;
|
||||
// Determine candidates by CSS context
|
||||
if (_.includes(wrapperClasses, 'context-range')) {
|
||||
// Suggestions for metric[|]
|
||||
return this.getRangeCompletionItems();
|
||||
} else if (_.includes(wrapperClasses, 'context-labels')) {
|
||||
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
|
||||
return this.getLabelCompletionItems.apply(this, arguments);
|
||||
} else if (_.includes(wrapperClasses, 'context-aggregation')) {
|
||||
return this.getAggregationCompletionItems.apply(this, arguments);
|
||||
} else if (
|
||||
// Show default suggestions in a couple of scenarios
|
||||
(prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token
|
||||
(prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace
|
||||
text.match(/[+\-*/^%]/) // Anything after binary operator
|
||||
) {
|
||||
return this.getEmptyCompletionItems(context || {});
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: [],
|
||||
};
|
||||
}
|
||||
|
||||
getEmptyCompletionItems(context: any): TypeaheadOutput {
|
||||
const { history } = context;
|
||||
const { metrics } = this;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
if (history && history.length > 0) {
|
||||
const historyItems = _.chain(history)
|
||||
.uniqBy('query')
|
||||
.take(HISTORY_ITEM_COUNT)
|
||||
.map(h => h.query)
|
||||
.map(wrapLabel)
|
||||
.map(item => addHistoryMetadata(item, history))
|
||||
.value();
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
skipSort: true,
|
||||
label: 'History',
|
||||
items: historyItems,
|
||||
});
|
||||
}
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
label: 'Functions',
|
||||
items: FUNCTIONS.map(setFunctionMove),
|
||||
});
|
||||
|
||||
if (metrics) {
|
||||
suggestions.push({
|
||||
label: 'Metrics',
|
||||
items: metrics.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
getRangeCompletionItems(): TypeaheadOutput {
|
||||
return {
|
||||
context: 'context-range',
|
||||
suggestions: [
|
||||
{
|
||||
label: 'Range vector',
|
||||
items: [...RATE_RANGES].map(wrapLabel),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
// sum(foo{bar="1"}) by (|)
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
// sum(foo{bar="1"}) by (
|
||||
const leftSide = line.slice(0, cursorOffset);
|
||||
const openParensAggregationIndex = leftSide.lastIndexOf('(');
|
||||
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
|
||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
||||
// foo{bar="1"}
|
||||
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
|
||||
|
||||
const labelKeys = this.labelKeys[selector];
|
||||
if (labelKeys) {
|
||||
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector);
|
||||
}
|
||||
|
||||
return {
|
||||
refresher,
|
||||
suggestions,
|
||||
context: 'context-aggregation',
|
||||
};
|
||||
}
|
||||
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
let context: string;
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
|
||||
// Get normalized selector
|
||||
let selector;
|
||||
let parsedSelector;
|
||||
try {
|
||||
parsedSelector = parseSelector(line, cursorOffset);
|
||||
selector = parsedSelector.selector;
|
||||
} catch {
|
||||
selector = EMPTY_SELECTOR;
|
||||
}
|
||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||
|
||||
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
|
||||
// Label values
|
||||
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
|
||||
const labelValues = this.labelValues[selector][labelKey];
|
||||
context = 'context-label-values';
|
||||
suggestions.push({
|
||||
label: `Label values for "${labelKey}"`,
|
||||
items: labelValues.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Label keys
|
||||
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
||||
if (labelKeys) {
|
||||
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||
if (possibleKeys.length > 0) {
|
||||
context = 'context-labels';
|
||||
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Query labels for selector
|
||||
// Temporarily add skip for logging
|
||||
if (selector && !this.labelValues[selector] && !this.supportsLogs) {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
// Query label values for default labels
|
||||
refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector, !containsMetric);
|
||||
}
|
||||
}
|
||||
|
||||
return { context, refresher, suggestions };
|
||||
}
|
||||
|
||||
async fetchMetricNames() {
|
||||
const url = '/api/v1/label/__name__/values';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
this.metrics = body.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchHistogramMetrics() {
|
||||
await this.fetchSeriesLabels(HISTOGRAM_SELECTOR, true);
|
||||
const histogramSeries = this.labelValues[HISTOGRAM_SELECTOR];
|
||||
if (histogramSeries && histogramSeries['__name__']) {
|
||||
this.histogramMetrics = histogramSeries['__name__'].slice().sort();
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily here while reusing this field for logging
|
||||
async fetchLogLabels() {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const labelKeys = body.data.slice().sort();
|
||||
const labelKeysBySelector = {
|
||||
...this.labelKeys,
|
||||
[EMPTY_SELECTOR]: labelKeys,
|
||||
};
|
||||
const labelValuesByKey = {};
|
||||
this.logLabelOptions = [];
|
||||
for (const key of labelKeys) {
|
||||
const valuesUrl = `/api/prom/label/${key}/values`;
|
||||
const res = await this.request(valuesUrl);
|
||||
const body = await (res.data || res.json());
|
||||
const values = body.data.slice().sort();
|
||||
labelValuesByKey[key] = values;
|
||||
this.logLabelOptions.push({
|
||||
label: key,
|
||||
value: key,
|
||||
children: values.map(value => ({ label: value, value })),
|
||||
});
|
||||
}
|
||||
this.labelValues = { [EMPTY_SELECTOR]: labelValuesByKey };
|
||||
this.labelKeys = labelKeysBySelector;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLabelValues(key: string) {
|
||||
const url = `/api/v1/label/${key}/values`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const exisingValues = this.labelValues[EMPTY_SELECTOR];
|
||||
const values = {
|
||||
...exisingValues,
|
||||
[key]: body.data,
|
||||
};
|
||||
this.labelValues = {
|
||||
...this.labelValues,
|
||||
[EMPTY_SELECTOR]: values,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSeriesLabels(name: string, withName?: boolean) {
|
||||
const url = `/api/v1/series?match[]=${name}`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const { keys, values } = processLabels(body.data, withName);
|
||||
this.labelKeys = {
|
||||
...this.labelKeys,
|
||||
[name]: keys,
|
||||
};
|
||||
this.labelValues = {
|
||||
...this.labelValues,
|
||||
[name]: values,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,9 +23,6 @@ export function processLabels(labels, withName = false) {
|
||||
return { values, keys: Object.keys(values) };
|
||||
}
|
||||
|
||||
// Strip syntax chars
|
||||
export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||
|
||||
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
|
||||
const selectorRegexp = /\{[^}]*?\}/;
|
||||
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
|
@ -1,6 +1,8 @@
|
||||
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 = [];
|
||||
|
||||
// ..._bucket metric needs a histogram_quantile()
|
||||
|
@ -0,0 +1,202 @@
|
||||
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' }]);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { parseSelector } from './prometheus';
|
||||
import { parseSelector } from '../language_utils';
|
||||
|
||||
describe('parseSelector()', () => {
|
||||
let parsed;
|
@ -1,3 +1,75 @@
|
||||
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 {
|
||||
value: string;
|
||||
label: string;
|
||||
@ -8,6 +80,26 @@ export interface HistoryItem {
|
||||
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 {
|
||||
from: string;
|
||||
to: string;
|
||||
@ -18,11 +110,28 @@ export interface Query {
|
||||
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 {
|
||||
id: string;
|
||||
done: boolean;
|
||||
error?: string;
|
||||
hints?: any[];
|
||||
hints?: QueryHint[];
|
||||
latency: number;
|
||||
options: any;
|
||||
query: string;
|
||||
|
Loading…
Reference in New Issue
Block a user