mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Use Explore's Prometheus editor in dashboard panel edit (#15364)
* WIP prometheus editor same in panel * Dont use panel in plugin editors * prettiered modified files * Fix step in external link * Prevent exiting edit mode when slate suggestions are shown * Blur behavior and $__interval variable * Remove unused query controller * Basic render test * Chore: Fixes blacklisted import * Refactor: Adds correct start and end time
This commit is contained in:
@@ -1,424 +0,0 @@
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import _ from 'lodash';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
export interface CompleterPosition {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface CompleterToken {
|
||||
type: string;
|
||||
value: string;
|
||||
row: number;
|
||||
column: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface CompleterSession {
|
||||
getTokenAt: (row: number, column: number) => CompleterToken;
|
||||
getTokens: (row: number) => CompleterToken[];
|
||||
}
|
||||
|
||||
export class PromCompleter {
|
||||
labelQueryCache: any;
|
||||
labelNameCache: any;
|
||||
labelValueCache: any;
|
||||
templateVariableCompletions: any;
|
||||
|
||||
identifierRegexps = [/\[/, /[a-zA-Z0-9_:]/];
|
||||
|
||||
constructor(private datasource: PrometheusDatasource, private templateSrv: TemplateSrv) {
|
||||
this.labelQueryCache = {};
|
||||
this.labelNameCache = {};
|
||||
this.labelValueCache = {};
|
||||
this.templateVariableCompletions = this.templateSrv.variables.map((variable: any) => {
|
||||
return {
|
||||
caption: '$' + variable.name,
|
||||
value: '$' + variable.name,
|
||||
meta: 'variable',
|
||||
score: Number.MAX_VALUE,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(editor: any, session: CompleterSession, pos: CompleterPosition, prefix: string, callback: Function) {
|
||||
const wrappedCallback = (err: any, completions: any[]) => {
|
||||
completions = completions.concat(this.templateVariableCompletions);
|
||||
return callback(err, completions);
|
||||
};
|
||||
|
||||
const token = session.getTokenAt(pos.row, pos.column);
|
||||
|
||||
switch (token.type) {
|
||||
case 'entity.name.tag.label-matcher':
|
||||
this.getCompletionsForLabelMatcherName(session, pos).then(completions => {
|
||||
wrappedCallback(null, completions);
|
||||
});
|
||||
return;
|
||||
case 'string.quoted.label-matcher':
|
||||
this.getCompletionsForLabelMatcherValue(session, pos).then(completions => {
|
||||
wrappedCallback(null, completions);
|
||||
});
|
||||
return;
|
||||
case 'entity.name.tag.label-list-matcher':
|
||||
this.getCompletionsForBinaryOperator(session, pos).then(completions => {
|
||||
wrappedCallback(null, completions);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.type === 'paren.lparen' && token.value === '[') {
|
||||
const vectors = [];
|
||||
for (const unit of ['s', 'm', 'h']) {
|
||||
for (const value of [1, 5, 10, 30]) {
|
||||
vectors.push({
|
||||
caption: value + unit,
|
||||
value: '[' + value + unit,
|
||||
meta: 'range vector',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vectors.unshift({
|
||||
caption: '$__interval_ms',
|
||||
value: '[$__interval_ms',
|
||||
meta: 'range vector',
|
||||
});
|
||||
|
||||
vectors.unshift({
|
||||
caption: '$__interval',
|
||||
value: '[$__interval',
|
||||
meta: 'range vector',
|
||||
});
|
||||
|
||||
wrappedCallback(null, vectors);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = prefix;
|
||||
|
||||
return this.datasource.performSuggestQuery(query, true).then((metricNames: string[]) => {
|
||||
wrappedCallback(
|
||||
null,
|
||||
metricNames.map(name => {
|
||||
let value = name;
|
||||
if (prefix === '(') {
|
||||
value = '(' + name;
|
||||
}
|
||||
|
||||
return {
|
||||
caption: name,
|
||||
value: value,
|
||||
meta: 'metric',
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getCompletionsForLabelMatcherName(session: CompleterSession, pos: CompleterPosition) {
|
||||
const metricName = this.findMetricName(session, pos.row, pos.column);
|
||||
if (!metricName) {
|
||||
return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name'));
|
||||
}
|
||||
|
||||
if (this.labelNameCache[metricName]) {
|
||||
return Promise.resolve(this.labelNameCache[metricName]);
|
||||
}
|
||||
|
||||
return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
|
||||
const labelNames = this.transformToCompletions(
|
||||
_.uniq(
|
||||
_.flatten(
|
||||
result.map((r: any) => {
|
||||
return Object.keys(r);
|
||||
})
|
||||
)
|
||||
),
|
||||
'label name'
|
||||
);
|
||||
this.labelNameCache[metricName] = labelNames;
|
||||
return Promise.resolve(labelNames);
|
||||
});
|
||||
}
|
||||
|
||||
getCompletionsForLabelMatcherValue(session: CompleterSession, pos: CompleterPosition) {
|
||||
const metricName = this.findMetricName(session, pos.row, pos.column);
|
||||
if (!metricName) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const labelNameToken = this.findToken(
|
||||
session,
|
||||
pos.row,
|
||||
pos.column,
|
||||
'entity.name.tag.label-matcher',
|
||||
null,
|
||||
'paren.lparen.label-matcher'
|
||||
);
|
||||
if (!labelNameToken) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const labelName = labelNameToken.value;
|
||||
|
||||
if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) {
|
||||
return Promise.resolve(this.labelValueCache[metricName][labelName]);
|
||||
}
|
||||
|
||||
return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
|
||||
const labelValues = this.transformToCompletions(
|
||||
_.uniq(
|
||||
result.map((r: any) => {
|
||||
return r[labelName];
|
||||
})
|
||||
),
|
||||
'label value'
|
||||
);
|
||||
this.labelValueCache[metricName] = this.labelValueCache[metricName] || {};
|
||||
this.labelValueCache[metricName][labelName] = labelValues;
|
||||
return Promise.resolve(labelValues);
|
||||
});
|
||||
}
|
||||
|
||||
getCompletionsForBinaryOperator(session: CompleterSession, pos: CompleterPosition) {
|
||||
const keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier');
|
||||
if (!keywordOperatorToken) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let rparenToken: CompleterToken, expr: string;
|
||||
switch (keywordOperatorToken.value) {
|
||||
case 'by':
|
||||
case 'without':
|
||||
rparenToken = this.findToken(
|
||||
session,
|
||||
keywordOperatorToken.row,
|
||||
keywordOperatorToken.column,
|
||||
'paren.rparen',
|
||||
null,
|
||||
'identifier'
|
||||
);
|
||||
if (!rparenToken) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
|
||||
if (expr === '') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
|
||||
const labelNames = this.transformToCompletions(
|
||||
_.uniq(
|
||||
_.flatten(
|
||||
result.map((r: any) => {
|
||||
return Object.keys(r);
|
||||
})
|
||||
)
|
||||
),
|
||||
'label name'
|
||||
);
|
||||
this.labelNameCache[expr] = labelNames;
|
||||
return labelNames;
|
||||
});
|
||||
case 'on':
|
||||
case 'ignoring':
|
||||
case 'group_left':
|
||||
case 'group_right':
|
||||
const binaryOperatorToken = this.findToken(
|
||||
session,
|
||||
keywordOperatorToken.row,
|
||||
keywordOperatorToken.column,
|
||||
'keyword.operator.binary',
|
||||
null,
|
||||
'identifier'
|
||||
);
|
||||
if (!binaryOperatorToken) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
rparenToken = this.findToken(
|
||||
session,
|
||||
binaryOperatorToken.row,
|
||||
binaryOperatorToken.column,
|
||||
'paren.rparen',
|
||||
null,
|
||||
'identifier'
|
||||
);
|
||||
if (rparenToken) {
|
||||
expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
|
||||
if (expr === '') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
|
||||
const labelNames = this.transformToCompletions(
|
||||
_.uniq(
|
||||
_.flatten(
|
||||
result.map((r: any) => {
|
||||
return Object.keys(r);
|
||||
})
|
||||
)
|
||||
),
|
||||
'label name'
|
||||
);
|
||||
this.labelNameCache[expr] = labelNames;
|
||||
return labelNames;
|
||||
});
|
||||
} else {
|
||||
const metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column);
|
||||
return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
|
||||
const labelNames = this.transformToCompletions(
|
||||
_.uniq(
|
||||
_.flatten(
|
||||
result.map((r: any) => {
|
||||
return Object.keys(r);
|
||||
})
|
||||
)
|
||||
),
|
||||
'label name'
|
||||
);
|
||||
this.labelNameCache[metricName] = labelNames;
|
||||
return Promise.resolve(labelNames);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getLabelNameAndValueForExpression(expr: string, type: string): Promise<any> {
|
||||
if (this.labelQueryCache[expr]) {
|
||||
return Promise.resolve(this.labelQueryCache[expr]);
|
||||
}
|
||||
let query = expr;
|
||||
if (type === 'metricName') {
|
||||
let op = '=~';
|
||||
if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(expr)) {
|
||||
op = '=';
|
||||
}
|
||||
query = '{__name__' + op + '"' + expr + '"}';
|
||||
}
|
||||
const { start, end } = this.datasource.getTimeRange();
|
||||
const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end;
|
||||
return this.datasource.metadataRequest(url).then((response: any) => {
|
||||
this.labelQueryCache[expr] = response.data.data;
|
||||
return response.data.data;
|
||||
});
|
||||
}
|
||||
|
||||
transformToCompletions(words: string[], meta: any) {
|
||||
return words.map(name => {
|
||||
return {
|
||||
caption: name,
|
||||
value: name,
|
||||
meta,
|
||||
score: Number.MAX_VALUE,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
findMetricName(session: CompleterSession, row: number, column: number) {
|
||||
let metricName = '';
|
||||
|
||||
let tokens: CompleterToken[];
|
||||
const nameLabelNameToken = this.findToken(
|
||||
session,
|
||||
row,
|
||||
column,
|
||||
'entity.name.tag.label-matcher',
|
||||
'__name__',
|
||||
'paren.lparen.label-matcher'
|
||||
);
|
||||
if (nameLabelNameToken) {
|
||||
tokens = session.getTokens(nameLabelNameToken.row);
|
||||
const nameLabelValueToken = tokens[nameLabelNameToken.index + 2];
|
||||
if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted.label-matcher') {
|
||||
metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation
|
||||
}
|
||||
} else {
|
||||
const metricNameToken = this.findToken(session, row, column, 'identifier', null, null);
|
||||
if (metricNameToken) {
|
||||
tokens = session.getTokens(metricNameToken.row);
|
||||
metricName = metricNameToken.value;
|
||||
}
|
||||
}
|
||||
|
||||
return metricName;
|
||||
}
|
||||
|
||||
findToken(session: CompleterSession, row: number, column: number, target: string, value: string, guard: string) {
|
||||
let tokens: CompleterToken[], idx: number;
|
||||
// find index and get column of previous token
|
||||
for (let r = row; r >= 0; r--) {
|
||||
let c: number;
|
||||
tokens = session.getTokens(r);
|
||||
if (r === row) {
|
||||
// current row
|
||||
c = 0;
|
||||
for (idx = 0; idx < tokens.length; idx++) {
|
||||
const nc = c + tokens[idx].value.length;
|
||||
if (nc >= column) {
|
||||
break;
|
||||
}
|
||||
c = nc;
|
||||
}
|
||||
} else {
|
||||
idx = tokens.length - 1;
|
||||
c =
|
||||
_.sum(
|
||||
tokens.map(t => {
|
||||
return t.value.length;
|
||||
})
|
||||
) - tokens[tokens.length - 1].value.length;
|
||||
}
|
||||
|
||||
for (; idx >= 0; idx--) {
|
||||
if (tokens[idx].type === guard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tokens[idx].type === target && (!value || tokens[idx].value === value)) {
|
||||
tokens[idx].row = r;
|
||||
tokens[idx].column = c;
|
||||
tokens[idx].index = idx;
|
||||
return tokens[idx];
|
||||
}
|
||||
c -= tokens[idx].value.length;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
findExpressionMatchedParen(session: CompleterSession, row: number, column: number) {
|
||||
let tokens: CompleterToken[], idx: number;
|
||||
let deep = 1;
|
||||
let expression = ')';
|
||||
for (let r = row; r >= 0; r--) {
|
||||
tokens = session.getTokens(r);
|
||||
if (r === row) {
|
||||
// current row
|
||||
let c = 0;
|
||||
for (idx = 0; idx < tokens.length; idx++) {
|
||||
c += tokens[idx].value.length;
|
||||
if (c >= column) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx = tokens.length - 1;
|
||||
}
|
||||
|
||||
for (; idx >= 0; idx--) {
|
||||
expression = tokens[idx].value + expression;
|
||||
if (tokens[idx].type === 'paren.rparen') {
|
||||
deep++;
|
||||
} else if (tokens[idx].type === 'paren.lparen') {
|
||||
deep--;
|
||||
if (deep === 0) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
import { DataQueryRequest, PanelData } from '@grafana/ui';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
interface Props {
|
||||
datasource: PrometheusDatasource;
|
||||
query: PromQuery;
|
||||
panelData: PanelData;
|
||||
}
|
||||
|
||||
interface State {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default class PromLink extends Component<Props, State> {
|
||||
state: State = { href: null };
|
||||
async componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.panelData !== this.props.panelData && this.props.panelData.request) {
|
||||
const href = await this.getExternalLink();
|
||||
this.setState({ href });
|
||||
}
|
||||
}
|
||||
|
||||
async getExternalLink(): Promise<string> {
|
||||
const { query, panelData } = this.props;
|
||||
const target = panelData.request.targets.length > 0 ? panelData.request.targets[0] : { datasource: null };
|
||||
const datasourceName = target.datasource;
|
||||
const datasource: PrometheusDatasource = datasourceName
|
||||
? (((await getDatasourceSrv().get(datasourceName)) as any) as PrometheusDatasource)
|
||||
: (this.props.datasource as PrometheusDatasource);
|
||||
|
||||
const range = panelData.request.range;
|
||||
const start = datasource.getPrometheusTime(range.from, false);
|
||||
const end = datasource.getPrometheusTime(range.to, true);
|
||||
const rangeDiff = Math.ceil(end - start);
|
||||
const endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
|
||||
|
||||
const options = {
|
||||
interval: panelData.request.interval,
|
||||
} as DataQueryRequest<PromQuery>;
|
||||
const queryOptions = datasource.createQuery(query, options, start, end);
|
||||
const expr = {
|
||||
'g0.expr': queryOptions.expr,
|
||||
'g0.range_input': rangeDiff + 's',
|
||||
'g0.end_input': endTime,
|
||||
'g0.step_input': queryOptions.step,
|
||||
'g0.tab': 0,
|
||||
};
|
||||
|
||||
const args = _.map(expr, (v: string, k: string) => {
|
||||
return k + '=' + encodeURIComponent(v);
|
||||
}).join('&');
|
||||
return `${datasource.directUrl}/graph?${args}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { href } = this.state;
|
||||
return (
|
||||
<a href={href} target="_blank">
|
||||
<i className="fa fa-share-square-o" /> Prometheus
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { dateTime } from '@grafana/ui';
|
||||
|
||||
import { PromQueryEditor } from './PromQueryEditor';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => {
|
||||
return {
|
||||
getTimeSrv: () => ({
|
||||
timeRange: () => ({
|
||||
from: dateTime(),
|
||||
to: dateTime(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const datasourceMock: unknown = {
|
||||
createQuery: jest.fn(q => q),
|
||||
getPrometheusTime: jest.fn((date, roundup) => 123),
|
||||
};
|
||||
const datasource: PrometheusDatasource = datasourceMock as PrometheusDatasource;
|
||||
const onRunQuery = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
const query: PromQuery = { expr: '', refId: 'A' };
|
||||
|
||||
const props: any = {
|
||||
datasource,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
query,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<PromQueryEditor {...props} />);
|
||||
const instance = wrapper.instance() as PromQueryEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render PromQueryEditor with basic options', () => {
|
||||
it('should render', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { FormLabel, Select, SelectOptionItem, Switch } from '@grafana/ui';
|
||||
import { QueryEditorProps, DataSourceStatus } from '@grafana/ui/src/types';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery, PromOptions } from '../types';
|
||||
|
||||
import PromQueryField from './PromQueryField';
|
||||
import PromLink from './PromLink';
|
||||
|
||||
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
|
||||
|
||||
const FORMAT_OPTIONS: Array<SelectOptionItem<string>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Heatmap', value: 'heatmap' },
|
||||
];
|
||||
|
||||
const INTERVAL_FACTOR_OPTIONS: Array<SelectOptionItem<number>> = _.map([1, 2, 3, 4, 5, 10], (value: number) => ({
|
||||
value,
|
||||
label: '1/' + value,
|
||||
}));
|
||||
|
||||
interface State {
|
||||
legendFormat: string;
|
||||
formatOption: SelectOptionItem<string>;
|
||||
interval: string;
|
||||
intervalFactorOption: SelectOptionItem<number>;
|
||||
instant: boolean;
|
||||
}
|
||||
|
||||
export class PromQueryEditor extends PureComponent<Props, State> {
|
||||
// Query target to be modified and used for queries
|
||||
query: PromQuery;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const { query } = props;
|
||||
this.query = query;
|
||||
// Query target properties that are fullu controlled inputs
|
||||
this.state = {
|
||||
// Fully controlled text inputs
|
||||
interval: query.interval,
|
||||
legendFormat: query.legendFormat,
|
||||
// Select options
|
||||
formatOption: FORMAT_OPTIONS.find(option => option.value === query.format) || FORMAT_OPTIONS[0],
|
||||
intervalFactorOption:
|
||||
INTERVAL_FACTOR_OPTIONS.find(option => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0],
|
||||
// Switch options
|
||||
instant: Boolean(query.instant),
|
||||
};
|
||||
}
|
||||
|
||||
onFieldChange = (query: PromQuery, override?) => {
|
||||
this.query.expr = query.expr;
|
||||
};
|
||||
|
||||
onFormatChange = (option: SelectOptionItem<string>) => {
|
||||
this.query.format = option.value;
|
||||
this.setState({ formatOption: option }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onInstantChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const instant = e.target.checked;
|
||||
this.query.instant = instant;
|
||||
this.setState({ instant }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onIntervalChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const interval = e.currentTarget.value;
|
||||
this.query.interval = interval;
|
||||
this.setState({ interval });
|
||||
};
|
||||
|
||||
onIntervalFactorChange = (option: SelectOptionItem<number>) => {
|
||||
this.query.intervalFactor = option.value;
|
||||
this.setState({ intervalFactorOption: option }, this.onRunQuery);
|
||||
};
|
||||
|
||||
onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const legendFormat = e.currentTarget.value;
|
||||
this.query.legendFormat = legendFormat;
|
||||
this.setState({ legendFormat });
|
||||
};
|
||||
|
||||
onRunQuery = () => {
|
||||
const { query } = this;
|
||||
this.props.onChange(query);
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, query, panelData, queryResponse } = this.props;
|
||||
const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="gf-form-input" style={{ height: 'initial' }}>
|
||||
<PromQueryField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onRunQuery={this.onRunQuery}
|
||||
onChange={this.onFieldChange}
|
||||
history={[]}
|
||||
panelData={panelData}
|
||||
queryResponse={queryResponse}
|
||||
datasourceStatus={DataSourceStatus.Connected} // TODO: replace with real DataSourceStatus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<FormLabel
|
||||
width={7}
|
||||
tooltip="Controls the name of the time series, using name or pattern. For example
|
||||
{{hostname}} will be replaced with label value for the label hostname."
|
||||
>
|
||||
Legend
|
||||
</FormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="legend format"
|
||||
value={legendFormat}
|
||||
onChange={this.onLegendChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<FormLabel
|
||||
width={7}
|
||||
tooltip="Leave blank for auto handling based on time range and panel width.
|
||||
Note that the actual dates used in the query will be adjusted
|
||||
to a multiple of the interval step."
|
||||
>
|
||||
Min step
|
||||
</FormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-8"
|
||||
placeholder={interval}
|
||||
onChange={this.onIntervalChange}
|
||||
value={interval}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Resolution</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={INTERVAL_FACTOR_OPTIONS}
|
||||
onChange={this.onIntervalFactorChange}
|
||||
value={intervalFactorOption}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Format</div>
|
||||
<Select isSearchable={false} options={FORMAT_OPTIONS} onChange={this.onFormatChange} value={formatOption} />
|
||||
<Switch label="Instant" checked={instant} onChange={this.onInstantChange} />
|
||||
|
||||
<FormLabel width={10} tooltip="Link to Graph in Prometheus">
|
||||
<PromLink
|
||||
datasource={datasource}
|
||||
query={this.query} // Use modified query
|
||||
panelData={panelData}
|
||||
/>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
import { PromQuery, PromContext } from '../types';
|
||||
import { PromQuery, PromContext, PromOptions } from '../types';
|
||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, QueryHint } from '@grafana/ui';
|
||||
import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, isSeriesData, toLegacyResponseData } from '@grafana/ui';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
@@ -101,7 +102,7 @@ interface CascaderOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<DataSourceApi<PromQuery>, PromQuery> {
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
|
||||
history: HistoryItem[];
|
||||
}
|
||||
|
||||
@@ -152,8 +153,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: PromQueryFieldProps) {
|
||||
const currentHasSeries = this.props.queryResponse.series && this.props.queryResponse.series.length > 0;
|
||||
if (currentHasSeries && prevProps.queryResponse.series !== this.props.queryResponse.series) {
|
||||
const { queryResponse } = this.props;
|
||||
const currentHasSeries = queryResponse && queryResponse.series && queryResponse.series.length > 0 ? true : false;
|
||||
if (currentHasSeries && prevProps.queryResponse && prevProps.queryResponse.series !== queryResponse.series) {
|
||||
this.refreshHint();
|
||||
}
|
||||
|
||||
@@ -175,11 +177,14 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
refreshHint = () => {
|
||||
const { datasource, query, queryResponse } = this.props;
|
||||
if (queryResponse.series && queryResponse.series.length === 0) {
|
||||
if (!queryResponse || !queryResponse.series || queryResponse.series.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hints = datasource.getQueryHints(query, queryResponse.series);
|
||||
const result = isSeriesData(queryResponse.series[0])
|
||||
? queryResponse.series.map(toLegacyResponseData)
|
||||
: queryResponse.series;
|
||||
const hints = datasource.getQueryHints(query, result);
|
||||
const hint = hints && hints.length > 0 ? hints[0] : null;
|
||||
this.setState({ hint });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="gf-form-input"
|
||||
style={
|
||||
Object {
|
||||
"height": "initial",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PromQueryField
|
||||
datasource={
|
||||
Object {
|
||||
"createQuery": [MockFunction],
|
||||
"getPrometheusTime": [MockFunction],
|
||||
}
|
||||
}
|
||||
datasourceStatus={0}
|
||||
history={Array []}
|
||||
onChange={[Function]}
|
||||
onRunQuery={[Function]}
|
||||
query={
|
||||
Object {
|
||||
"expr": "",
|
||||
"refId": "A",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<Component
|
||||
tooltip="Controls the name of the time series, using name or pattern. For example
|
||||
{{hostname}} will be replaced with label value for the label hostname."
|
||||
width={7}
|
||||
>
|
||||
Legend
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="legend format"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<Component
|
||||
tooltip="Leave blank for auto handling based on time range and panel width.
|
||||
Note that the actual dates used in the query will be adjusted
|
||||
to a multiple of the interval step."
|
||||
width={7}
|
||||
>
|
||||
Min step
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input width-8"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<div
|
||||
className="gf-form-label"
|
||||
>
|
||||
Resolution
|
||||
</div>
|
||||
<Select
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
components={
|
||||
Object {
|
||||
"Group": [Function],
|
||||
"IndicatorsContainer": [Function],
|
||||
"MenuList": [Function],
|
||||
"Option": [Function],
|
||||
"SingleValue": [Function],
|
||||
}
|
||||
}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={300}
|
||||
onChange={[Function]}
|
||||
openMenuOnFocus={false}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "1/1",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"label": "1/2",
|
||||
"value": 2,
|
||||
},
|
||||
Object {
|
||||
"label": "1/3",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"label": "1/4",
|
||||
"value": 4,
|
||||
},
|
||||
Object {
|
||||
"label": "1/5",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"label": "1/10",
|
||||
"value": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"label": "1/1",
|
||||
"value": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<div
|
||||
className="gf-form-label"
|
||||
>
|
||||
Format
|
||||
</div>
|
||||
<Select
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
components={
|
||||
Object {
|
||||
"Group": [Function],
|
||||
"IndicatorsContainer": [Function],
|
||||
"MenuList": [Function],
|
||||
"Option": [Function],
|
||||
"SingleValue": [Function],
|
||||
}
|
||||
}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
maxMenuHeight={300}
|
||||
onChange={[Function]}
|
||||
openMenuOnFocus={false}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "Time series",
|
||||
"value": "time_series",
|
||||
},
|
||||
Object {
|
||||
"label": "Table",
|
||||
"value": "table",
|
||||
},
|
||||
Object {
|
||||
"label": "Heatmap",
|
||||
"value": "heatmap",
|
||||
},
|
||||
]
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"label": "Time series",
|
||||
"value": "time_series",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={false}
|
||||
label="Instant"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<Component
|
||||
tooltip="Link to Graph in Prometheus"
|
||||
width={10}
|
||||
>
|
||||
<PromLink
|
||||
datasource={
|
||||
Object {
|
||||
"createQuery": [MockFunction],
|
||||
"getPrometheusTime": [MockFunction],
|
||||
}
|
||||
}
|
||||
query={
|
||||
Object {
|
||||
"expr": "",
|
||||
"refId": "A",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,532 +0,0 @@
|
||||
// jshint ignore: start
|
||||
// jscs: disable
|
||||
ace.define("ace/mode/prometheus_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
|
||||
|
||||
var PrometheusHighlightRules = function() {
|
||||
var keywords = (
|
||||
"count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile"
|
||||
);
|
||||
|
||||
var builtinConstants = (
|
||||
"true|false|null|__name__|job"
|
||||
);
|
||||
|
||||
var builtinFunctions = (
|
||||
"abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv|" + "drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2|" +
|
||||
"log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time|" +
|
||||
"min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time"
|
||||
);
|
||||
|
||||
var keywordMapper = this.createKeywordMapper({
|
||||
"support.function": builtinFunctions,
|
||||
"keyword": keywords,
|
||||
"constant.language": builtinConstants
|
||||
}, "identifier", true);
|
||||
|
||||
this.$rules = {
|
||||
"start" : [ {
|
||||
token : "string", // single line
|
||||
regex : /"(?:[^"\\]|\\.)*?"/
|
||||
}, {
|
||||
token : "string", // string
|
||||
regex : "'.*?'"
|
||||
}, {
|
||||
token : "constant.numeric", // float
|
||||
regex : "[-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
|
||||
}, {
|
||||
token : "constant.language", // time
|
||||
regex : "\\d+[smhdwy]"
|
||||
}, {
|
||||
token : "keyword.operator.binary",
|
||||
regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>|and|or|unless"
|
||||
}, {
|
||||
token : "keyword.other",
|
||||
regex : "keep_common|offset|bool"
|
||||
}, {
|
||||
token : "keyword.control",
|
||||
regex : "by|without|on|ignoring|group_left|group_right",
|
||||
next : "start-label-list-matcher"
|
||||
}, {
|
||||
token : "variable",
|
||||
regex : "\\$[A-Za-z0-9_]+"
|
||||
}, {
|
||||
token : keywordMapper,
|
||||
regex : "[a-zA-Z_:][a-zA-Z0-9_:]*"
|
||||
}, {
|
||||
token : "paren.lparen",
|
||||
regex : "[[(]"
|
||||
}, {
|
||||
token : "paren.lparen.label-matcher",
|
||||
regex : "{",
|
||||
next : "start-label-matcher"
|
||||
}, {
|
||||
token : "paren.rparen",
|
||||
regex : "[\\])]"
|
||||
}, {
|
||||
token : "paren.rparen.label-matcher",
|
||||
regex : "}"
|
||||
}, {
|
||||
token : "text",
|
||||
regex : "\\s+"
|
||||
} ],
|
||||
"start-label-matcher" : [ {
|
||||
token : "entity.name.tag.label-matcher",
|
||||
regex : '[a-zA-Z_][a-zA-Z0-9_]*'
|
||||
}, {
|
||||
token : "keyword.operator.label-matcher",
|
||||
regex : '=~|=|!~|!='
|
||||
}, {
|
||||
token : "string.quoted.label-matcher",
|
||||
regex : '"[^"]*"|\'[^\']*\''
|
||||
}, {
|
||||
token : "punctuation.operator.label-matcher",
|
||||
regex : ","
|
||||
}, {
|
||||
token : "paren.rparen.label-matcher",
|
||||
regex : "}",
|
||||
next : "start"
|
||||
} ],
|
||||
"start-label-list-matcher" : [ {
|
||||
token : "paren.lparen.label-list-matcher",
|
||||
regex : "[(]"
|
||||
}, {
|
||||
token : "entity.name.tag.label-list-matcher",
|
||||
regex : '[a-zA-Z_][a-zA-Z0-9_]*'
|
||||
}, {
|
||||
token : "punctuation.operator.label-list-matcher",
|
||||
regex : ","
|
||||
}, {
|
||||
token : "paren.rparen.label-list-matcher",
|
||||
regex : "[)]",
|
||||
next : "start"
|
||||
} ]
|
||||
};
|
||||
|
||||
this.normalizeRules();
|
||||
};
|
||||
|
||||
oop.inherits(PrometheusHighlightRules, TextHighlightRules);
|
||||
|
||||
exports.PrometheusHighlightRules = PrometheusHighlightRules;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/prometheus_completions",["require","exports","module","ace/token_iterator", "ace/lib/lang"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var lang = require("../lib/lang");
|
||||
|
||||
var prometheusKeyWords = [
|
||||
"by", "without", "keep_common", "offset", "bool", "and", "or", "unless", "ignoring", "on", "group_left",
|
||||
"group_right", "count", "count_values", "min", "max", "avg", "sum", "stddev", "stdvar", "bottomk", "topk", "quantile"
|
||||
];
|
||||
|
||||
var keyWordsCompletions = prometheusKeyWords.map(function(word) {
|
||||
return {
|
||||
caption: word,
|
||||
value: word,
|
||||
meta: "keyword",
|
||||
score: Number.MAX_VALUE
|
||||
}
|
||||
});
|
||||
|
||||
var prometheusFunctions = [
|
||||
{
|
||||
name: 'abs()', value: 'abs',
|
||||
def: 'abs(v instant-vector)',
|
||||
docText: 'Returns the input vector with all sample values converted to their absolute value.'
|
||||
},
|
||||
{
|
||||
name: 'absent()', value: 'absent',
|
||||
def: 'absent(v instant-vector)',
|
||||
docText: 'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.'
|
||||
},
|
||||
{
|
||||
name: 'ceil()', value: 'ceil',
|
||||
def: 'ceil(v instant-vector)',
|
||||
docText: 'Rounds the sample values of all elements in `v` up to the nearest integer.'
|
||||
},
|
||||
{
|
||||
name: 'changes()', value: 'changes',
|
||||
def: 'changes(v range-vector)',
|
||||
docText: 'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.'
|
||||
},
|
||||
{
|
||||
name: 'clamp_max()', value: 'clamp_max',
|
||||
def: 'clamp_max(v instant-vector, max scalar)',
|
||||
docText: 'Clamps the sample values of all elements in `v` to have an upper limit of `max`.'
|
||||
},
|
||||
{
|
||||
name: 'clamp_min()', value: 'clamp_min',
|
||||
def: 'clamp_min(v instant-vector, min scalar)',
|
||||
docText: 'Clamps the sample values of all elements in `v` to have a lower limit of `min`.'
|
||||
},
|
||||
{
|
||||
name: 'count_scalar()', value: 'count_scalar',
|
||||
def: 'count_scalar(v instant-vector)',
|
||||
docText: 'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.'
|
||||
},
|
||||
{
|
||||
name: 'day_of_month()', value: 'day_of_month',
|
||||
def: 'day_of_month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.'
|
||||
},
|
||||
{
|
||||
name: 'day_of_week()', value: 'day_of_week',
|
||||
def: 'day_of_week(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.'
|
||||
},
|
||||
{
|
||||
name: 'days_in_month()', value: 'days_in_month',
|
||||
def: 'days_in_month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.'
|
||||
},
|
||||
{
|
||||
name: 'delta()', value: 'delta',
|
||||
def: 'delta(v range-vector)',
|
||||
docText: 'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.'
|
||||
},
|
||||
{
|
||||
name: 'deriv()', value: 'deriv',
|
||||
def: 'deriv(v range-vector)',
|
||||
docText: 'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.'
|
||||
},
|
||||
{
|
||||
name: 'drop_common_labels()', value: 'drop_common_labels',
|
||||
def: 'drop_common_labels(instant-vector)',
|
||||
docText: 'Drops all labels that have the same name and value across all series in the input vector.'
|
||||
},
|
||||
{
|
||||
name: 'exp()', value: 'exp',
|
||||
def: 'exp(v instant-vector)',
|
||||
docText: 'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`'
|
||||
},
|
||||
{
|
||||
name: 'floor()', value: 'floor',
|
||||
def: 'floor(v instant-vector)',
|
||||
docText: 'Rounds the sample values of all elements in `v` down to the nearest integer.'
|
||||
},
|
||||
{
|
||||
name: 'histogram_quantile()', value: 'histogram_quantile',
|
||||
def: 'histogram_quantile(φ float, b instant-vector)',
|
||||
docText: 'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.'
|
||||
},
|
||||
{
|
||||
name: 'holt_winters()', value: 'holt_winters',
|
||||
def: 'holt_winters(v range-vector, sf scalar, tf scalar)',
|
||||
docText: 'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.'
|
||||
},
|
||||
{
|
||||
name: 'hour()', value: 'hour',
|
||||
def: 'hour(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.'
|
||||
},
|
||||
{
|
||||
name: 'idelta()', value: 'idelta',
|
||||
def: 'idelta(v range-vector)',
|
||||
docText: 'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.'
|
||||
},
|
||||
{
|
||||
name: 'increase()', value: 'increase',
|
||||
def: 'increase(v range-vector)',
|
||||
docText: 'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.'
|
||||
},
|
||||
{
|
||||
name: 'irate()', value: 'irate',
|
||||
def: 'irate(v range-vector)',
|
||||
docText: 'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.'
|
||||
},
|
||||
{
|
||||
name: 'label_replace()', value: 'label_replace',
|
||||
def: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)',
|
||||
docText: 'For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn\'t match then the timeseries is returned unchanged.'
|
||||
},
|
||||
{
|
||||
name: 'ln()', value: 'ln',
|
||||
def: 'ln(v instant-vector)',
|
||||
docText: 'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`'
|
||||
},
|
||||
{
|
||||
name: 'log2()', value: 'log2',
|
||||
def: 'log2(v instant-vector)',
|
||||
docText: 'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.'
|
||||
},
|
||||
{
|
||||
name: 'log10()', value: 'log10',
|
||||
def: 'log10(v instant-vector)',
|
||||
docText: 'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.'
|
||||
},
|
||||
{
|
||||
name: 'minute()', value: 'minute',
|
||||
def: 'minute(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.'
|
||||
},
|
||||
{
|
||||
name: 'month()', value: 'month',
|
||||
def: 'month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.'
|
||||
},
|
||||
{
|
||||
name: 'predict_linear()', value: 'predict_linear',
|
||||
def: 'predict_linear(v range-vector, t scalar)',
|
||||
docText: 'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.'
|
||||
},
|
||||
{
|
||||
name: 'rate()', value: 'rate',
|
||||
def: 'rate(v range-vector)',
|
||||
docText: "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period."
|
||||
},
|
||||
{
|
||||
name: 'resets()', value: 'resets',
|
||||
def: 'resets(v range-vector)',
|
||||
docText: 'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.'
|
||||
},
|
||||
{
|
||||
name: 'round()', value: 'round',
|
||||
def: 'round(v instant-vector, to_nearest=1 scalar)',
|
||||
docText: 'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.'
|
||||
},
|
||||
{
|
||||
name: 'scalar()', value: 'scalar',
|
||||
def: 'scalar(v instant-vector)',
|
||||
docText: 'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.'
|
||||
},
|
||||
{
|
||||
name: 'sort()', value: 'sort',
|
||||
def: 'sort(v instant-vector)',
|
||||
docText: 'Returns vector elements sorted by their sample values, in ascending order.'
|
||||
},
|
||||
{
|
||||
name: 'sort_desc()', value: 'sort_desc',
|
||||
def: 'sort_desc(v instant-vector)',
|
||||
docText: 'Returns vector elements sorted by their sample values, in descending order.'
|
||||
},
|
||||
{
|
||||
name: 'sqrt()', value: 'sqrt',
|
||||
def: 'sqrt(v instant-vector)',
|
||||
docText: 'Calculates the square root of all elements in `v`.'
|
||||
},
|
||||
{
|
||||
name: 'time()', value: 'time',
|
||||
def: 'time()',
|
||||
docText: 'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.'
|
||||
},
|
||||
{
|
||||
name: 'vector()', value: 'vector',
|
||||
def: 'vector(s scalar)',
|
||||
docText: 'Returns the scalar `s` as a vector with no labels.'
|
||||
},
|
||||
{
|
||||
name: 'year()', value: 'year',
|
||||
def: 'year(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the year for each of the given times in UTC.'
|
||||
},
|
||||
{
|
||||
name: 'avg_over_time()', value: 'avg_over_time',
|
||||
def: 'avg_over_time(range-vector)',
|
||||
docText: 'The average value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'min_over_time()', value: 'min_over_time',
|
||||
def: 'min_over_time(range-vector)',
|
||||
docText: 'The minimum value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'max_over_time()', value: 'max_over_time',
|
||||
def: 'max_over_time(range-vector)',
|
||||
docText: 'The maximum value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'sum_over_time()', value: 'sum_over_time',
|
||||
def: 'sum_over_time(range-vector)',
|
||||
docText: 'The sum of all values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'count_over_time()', value: 'count_over_time',
|
||||
def: 'count_over_time(range-vector)',
|
||||
docText: 'The count of all values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'quantile_over_time()', value: 'quantile_over_time',
|
||||
def: 'quantile_over_time(scalar, range-vector)',
|
||||
docText: 'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'stddev_over_time()', value: 'stddev_over_time',
|
||||
def: 'stddev_over_time(range-vector)',
|
||||
docText: 'The population standard deviation of the values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'stdvar_over_time()', value: 'stdvar_over_time',
|
||||
def: 'stdvar_over_time(range-vector)',
|
||||
docText: 'The population standard variance of the values in the specified interval.'
|
||||
},
|
||||
];
|
||||
|
||||
function wrapText(str, len) {
|
||||
len = len || 60;
|
||||
var lines = [];
|
||||
var space_index = 0;
|
||||
var line_start = 0;
|
||||
var next_line_end = len;
|
||||
var line = "";
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
if (str[i] === ' ') {
|
||||
space_index = i;
|
||||
} else if (i >= next_line_end && space_index != 0) {
|
||||
line = str.slice(line_start, space_index);
|
||||
lines.push(line);
|
||||
line_start = space_index + 1;
|
||||
next_line_end = i + len;
|
||||
space_index = 0;
|
||||
}
|
||||
}
|
||||
line = str.slice(line_start);
|
||||
lines.push(line);
|
||||
return lines.join(" <br>");
|
||||
}
|
||||
|
||||
function convertMarkDownTags(text) {
|
||||
text = text.replace(/```(.+)```/, "<pre>$1</pre>");
|
||||
text = text.replace(/`([^`]+)`/, "<code>$1</code>");
|
||||
return text;
|
||||
}
|
||||
|
||||
function convertToHTML(item) {
|
||||
var docText = lang.escapeHTML(item.docText);
|
||||
docText = convertMarkDownTags(wrapText(docText, 40));
|
||||
return [
|
||||
"<b>", lang.escapeHTML(item.def), "</b>", "<hr></hr>", docText, "<br> "
|
||||
].join("");
|
||||
}
|
||||
|
||||
var functionsCompletions = prometheusFunctions.map(function(item) {
|
||||
return {
|
||||
caption: item.name,
|
||||
value: item.value,
|
||||
docHTML: convertToHTML(item),
|
||||
meta: "function",
|
||||
score: Number.MAX_VALUE
|
||||
};
|
||||
});
|
||||
|
||||
var PrometheusCompletions = function() {};
|
||||
|
||||
(function() {
|
||||
this.getCompletions = function(state, session, pos, prefix, callback) {
|
||||
var token = session.getTokenAt(pos.row, pos.column);
|
||||
if (token.type === 'entity.name.tag.label-matcher'
|
||||
|| token.type === 'string.quoted.label-matcher'
|
||||
|| token.type === 'entity.name.tag.label-list-matcher') {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var completions = keyWordsCompletions.concat(functionsCompletions);
|
||||
callback(null, completions);
|
||||
};
|
||||
|
||||
}).call(PrometheusCompletions.prototype);
|
||||
|
||||
exports.PrometheusCompletions = PrometheusCompletions;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/behaviour/prometheus",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle","ace/token_iterator"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../../lib/oop");
|
||||
var Behaviour = require("../behaviour").Behaviour;
|
||||
var CstyleBehaviour = require("./cstyle").CstyleBehaviour;
|
||||
var TokenIterator = require("../../token_iterator").TokenIterator;
|
||||
|
||||
function getWrapped(selection, selected, opening, closing) {
|
||||
var rowDiff = selection.end.row - selection.start.row;
|
||||
return {
|
||||
text: opening + selected + closing,
|
||||
selection: [
|
||||
0,
|
||||
selection.start.column + 1,
|
||||
rowDiff,
|
||||
selection.end.column + (rowDiff ? 0 : 1)
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
var PrometheusBehaviour = function () {
|
||||
this.inherit(CstyleBehaviour);
|
||||
|
||||
// Rewrite default CstyleBehaviour for {} braces
|
||||
this.add("braces", "insertion", function(state, action, editor, session, text) {
|
||||
if (text == '{') {
|
||||
var selection = editor.getSelectionRange();
|
||||
var selected = session.doc.getTextRange(selection);
|
||||
if (selected !== "" && editor.getWrapBehavioursEnabled()) {
|
||||
return getWrapped(selection, selected, '{', '}');
|
||||
} else if (CstyleBehaviour.isSaneInsertion(editor, session)) {
|
||||
return {
|
||||
text: '{}',
|
||||
selection: [1, 1]
|
||||
};
|
||||
}
|
||||
} else if (text == '}') {
|
||||
var cursor = editor.getCursorPosition();
|
||||
var line = session.doc.getLine(cursor.row);
|
||||
var rightChar = line.substring(cursor.column, cursor.column + 1);
|
||||
if (rightChar == '}') {
|
||||
var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row});
|
||||
if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) {
|
||||
return {
|
||||
text: '',
|
||||
selection: [1, 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.add("braces", "deletion", function(state, action, editor, session, range) {
|
||||
var selected = session.doc.getTextRange(range);
|
||||
if (!range.isMultiLine() && selected == '{') {
|
||||
var line = session.doc.getLine(range.start.row);
|
||||
var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
|
||||
if (rightChar == '}') {
|
||||
range.end.column++;
|
||||
return range;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
oop.inherits(PrometheusBehaviour, CstyleBehaviour);
|
||||
|
||||
exports.PrometheusBehaviour = PrometheusBehaviour;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/prometheus",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/prometheus_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextMode = require("./text").Mode;
|
||||
var PrometheusHighlightRules = require("./prometheus_highlight_rules").PrometheusHighlightRules;
|
||||
var PrometheusCompletions = require("./prometheus_completions").PrometheusCompletions;
|
||||
var PrometheusBehaviour = require("./behaviour/prometheus").PrometheusBehaviour;
|
||||
|
||||
var Mode = function() {
|
||||
this.HighlightRules = PrometheusHighlightRules;
|
||||
this.$behaviour = new PrometheusBehaviour();
|
||||
this.$completer = new PrometheusCompletions();
|
||||
// replace keyWordCompleter
|
||||
this.completer = this.$completer;
|
||||
};
|
||||
oop.inherits(Mode, TextMode);
|
||||
|
||||
(function() {
|
||||
|
||||
this.$id = "ace/mode/prometheus";
|
||||
}).call(Mode.prototype);
|
||||
|
||||
exports.Mode = Mode;
|
||||
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { PrometheusQueryCtrl } from './query_ctrl';
|
||||
import { PromQueryEditor } from './components/PromQueryEditor';
|
||||
import { PrometheusConfigCtrl } from './config_ctrl';
|
||||
|
||||
import PrometheusStartPage from './components/PromStart';
|
||||
@@ -11,7 +11,7 @@ class PrometheusAnnotationsQueryCtrl {
|
||||
|
||||
export {
|
||||
PrometheusDatasource as Datasource,
|
||||
PrometheusQueryCtrl as QueryCtrl,
|
||||
PromQueryEditor as QueryEditor,
|
||||
PrometheusConfigCtrl as ConfigCtrl,
|
||||
PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
PromQueryField as ExploreQueryField,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()" get-completer="ctrl.getCompleter()"
|
||||
data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Legend</label>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
|
||||
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Controls the name of the time series, using name or pattern. For example
|
||||
<span ng-non-bindable>{{hostname}}</span> will be replaced with label value for the label hostname.
|
||||
</info-popover>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6">Min step</label>
|
||||
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.target.interval" data-placement="right" spellcheck='false'
|
||||
placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()"
|
||||
/>
|
||||
<info-popover mode="right-absolute">
|
||||
Leave blank for auto handling based on time range and panel width. Note that the actual dates used in the query will be adjusted
|
||||
to a multiple of the interval step.
|
||||
</info-popover>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Resolution</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select ng-model="ctrl.target.intervalFactor" class="gf-form-input" ng-options="r.factor as r.label for r in ctrl.resolutions"
|
||||
ng-change="ctrl.refreshMetricData()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-5">Format</label>
|
||||
<div class="gf-form-select-wrapper width-8">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Instant" label-class="width-5" checked="ctrl.target.instant" on-change="ctrl.refresh()">
|
||||
</gf-form-switch>
|
||||
<label class="gf-form-label gf-form-label--grow">
|
||||
<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
|
||||
<i class="fa fa-share-square-o"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</query-editor-row>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { CompletionItem } from 'app/types/explore';
|
||||
|
||||
export const RATE_RANGES: CompletionItem[] = [
|
||||
{ label: '$__interval', sortText: '$__interval' },
|
||||
{ label: '1m', sortText: '00:01:00' },
|
||||
{ label: '5m', sortText: '00:05:00' },
|
||||
{ label: '10m', sortText: '00:10:00' },
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { PromCompleter } from './completer';
|
||||
import './mode-prometheus';
|
||||
import './snippets/prometheus';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
class PrometheusQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
metric: any;
|
||||
resolutions: any;
|
||||
formats: any;
|
||||
instant: any;
|
||||
oldTarget: any;
|
||||
suggestMetrics: any;
|
||||
getMetricsAutocomplete: any;
|
||||
linkToPrometheus: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any, $injector: angular.auto.IInjectorService, private templateSrv: TemplateSrv) {
|
||||
super($scope, $injector);
|
||||
|
||||
const target = this.target;
|
||||
target.expr = target.expr || '';
|
||||
target.intervalFactor = target.intervalFactor || 1;
|
||||
target.format = target.format || this.getDefaultFormat();
|
||||
|
||||
this.metric = '';
|
||||
this.resolutions = _.map([1, 2, 3, 4, 5, 10], f => {
|
||||
return { factor: f, label: '1/' + f };
|
||||
});
|
||||
|
||||
this.formats = [
|
||||
{ text: 'Time series', value: 'time_series' },
|
||||
{ text: 'Table', value: 'table' },
|
||||
{ text: 'Heatmap', value: 'heatmap' },
|
||||
];
|
||||
|
||||
this.instant = false;
|
||||
|
||||
this.updateLink();
|
||||
}
|
||||
|
||||
getCompleter(query: string) {
|
||||
return new PromCompleter(this.datasource, this.templateSrv);
|
||||
}
|
||||
|
||||
getDefaultFormat() {
|
||||
if (this.panelCtrl.panel.type === 'table') {
|
||||
return 'table';
|
||||
} else if (this.panelCtrl.panel.type === 'heatmap') {
|
||||
return 'heatmap';
|
||||
}
|
||||
|
||||
return 'time_series';
|
||||
}
|
||||
|
||||
refreshMetricData() {
|
||||
if (!_.isEqual(this.oldTarget, this.target)) {
|
||||
this.oldTarget = angular.copy(this.target);
|
||||
this.panelCtrl.refresh();
|
||||
this.updateLink();
|
||||
}
|
||||
}
|
||||
|
||||
updateLink() {
|
||||
const range = this.panelCtrl.range;
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
|
||||
const endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
|
||||
const expr = {
|
||||
'g0.expr': this.templateSrv.replace(
|
||||
this.target.expr,
|
||||
this.panelCtrl.panel.scopedVars,
|
||||
this.datasource.interpolateQueryExpr
|
||||
),
|
||||
'g0.range_input': rangeDiff + 's',
|
||||
'g0.end_input': endTime,
|
||||
'g0.step_input': this.target.step,
|
||||
'g0.stacked': this.panelCtrl.panel.stack ? 1 : 0,
|
||||
'g0.tab': 0,
|
||||
};
|
||||
const args = _.map(expr, (v, k) => {
|
||||
return k + '=' + encodeURIComponent(v);
|
||||
}).join('&');
|
||||
this.linkToPrometheus = this.datasource.directUrl + '/graph?' + args;
|
||||
}
|
||||
}
|
||||
|
||||
export { PrometheusQueryCtrl };
|
||||
@@ -1,21 +0,0 @@
|
||||
// jshint ignore: start
|
||||
// jscs: disable
|
||||
ace.define("ace/snippets/prometheus",["require","exports","module"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
// exports.snippetText = "# rate\n\
|
||||
// snippet r\n\
|
||||
// rate(${1:metric}[${2:range}])\n\
|
||||
// ";
|
||||
|
||||
exports.snippets = [
|
||||
{
|
||||
"content": "rate(${1:metric}[${2:range}])",
|
||||
"name": "rate()",
|
||||
"scope": "prometheus",
|
||||
"tabTrigger": "r"
|
||||
}
|
||||
];
|
||||
|
||||
exports.scope = "prometheus";
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { PromCompleter } from '../completer';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DataSourceInstanceSettings } from '@grafana/ui';
|
||||
import { PromOptions } from '../types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { IQService } from 'angular';
|
||||
jest.mock('../datasource');
|
||||
jest.mock('@grafana/ui');
|
||||
|
||||
describe('Prometheus editor completer', () => {
|
||||
function getSessionStub(data: any) {
|
||||
return {
|
||||
getTokenAt: jest.fn(() => data.currentToken),
|
||||
getTokens: jest.fn(() => data.tokens),
|
||||
getLine: jest.fn(() => data.line),
|
||||
};
|
||||
}
|
||||
|
||||
const editor = {};
|
||||
|
||||
const backendSrv = {} as BackendSrv;
|
||||
const datasourceStub = new PrometheusDatasource(
|
||||
{} as DataSourceInstanceSettings<PromOptions>,
|
||||
{} as IQService,
|
||||
backendSrv,
|
||||
{} as TemplateSrv,
|
||||
{} as TimeSrv
|
||||
);
|
||||
|
||||
datasourceStub.metadataRequest = jest.fn(() =>
|
||||
Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } })
|
||||
);
|
||||
datasourceStub.getTimeRange = jest.fn(() => {
|
||||
return { start: 1514732400, end: 1514818800 };
|
||||
});
|
||||
datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu']));
|
||||
|
||||
const templateSrv: TemplateSrv = ({
|
||||
variables: [
|
||||
{
|
||||
name: 'var_name',
|
||||
options: [{ text: 'foo', value: 'foo', selected: false }, { text: 'bar', value: 'bar', selected: true }],
|
||||
},
|
||||
],
|
||||
} as any) as TemplateSrv;
|
||||
const completer = new PromCompleter(datasourceStub, templateSrv);
|
||||
|
||||
describe('When inside brackets', () => {
|
||||
it('Should return range vectors', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: { type: 'paren.lparen', value: '[', index: 2, start: 9 },
|
||||
tokens: [{ type: 'identifier', value: 'node_cpu' }, { type: 'paren.lparen', value: '[' }],
|
||||
line: 'node_cpu[',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s: any, res: any) => {
|
||||
expect(res[0].caption).toEqual('$__interval');
|
||||
expect(res[0].value).toEqual('[$__interval');
|
||||
expect(res[0].meta).toEqual('range vector');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label name', () => {
|
||||
it('Should return label name list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {
|
||||
type: 'entity.name.tag.label-matcher',
|
||||
value: 'j',
|
||||
index: 2,
|
||||
start: 9,
|
||||
},
|
||||
tokens: [
|
||||
{ type: 'identifier', value: 'node_cpu' },
|
||||
{ type: 'paren.lparen.label-matcher', value: '{' },
|
||||
{
|
||||
type: 'entity.name.tag.label-matcher',
|
||||
value: 'j',
|
||||
index: 2,
|
||||
start: 9,
|
||||
},
|
||||
{ type: 'paren.rparen.label-matcher', value: '}' },
|
||||
],
|
||||
line: 'node_cpu{j}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s: any, res: any) => {
|
||||
expect(res[0].meta).toEqual('label name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label name with __name__ match', () => {
|
||||
it('Should return label name list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {
|
||||
type: 'entity.name.tag.label-matcher',
|
||||
value: 'j',
|
||||
index: 5,
|
||||
start: 22,
|
||||
},
|
||||
tokens: [
|
||||
{ type: 'paren.lparen.label-matcher', value: '{' },
|
||||
{ type: 'entity.name.tag.label-matcher', value: '__name__' },
|
||||
{ type: 'keyword.operator.label-matcher', value: '=~' },
|
||||
{ type: 'string.quoted.label-matcher', value: '"node_cpu"' },
|
||||
{ type: 'punctuation.operator.label-matcher', value: ',' },
|
||||
{
|
||||
type: 'entity.name.tag.label-matcher',
|
||||
value: 'j',
|
||||
index: 5,
|
||||
start: 22,
|
||||
},
|
||||
{ type: 'paren.rparen.label-matcher', value: '}' },
|
||||
],
|
||||
line: '{__name__=~"node_cpu",j}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s: any, res: any) => {
|
||||
expect(res[0].meta).toEqual('label name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label value', () => {
|
||||
it('Should return label value list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {
|
||||
type: 'string.quoted.label-matcher',
|
||||
value: '"n"',
|
||||
index: 4,
|
||||
start: 13,
|
||||
},
|
||||
tokens: [
|
||||
{ type: 'identifier', value: 'node_cpu' },
|
||||
{ type: 'paren.lparen.label-matcher', value: '{' },
|
||||
{ type: 'entity.name.tag.label-matcher', value: 'job' },
|
||||
{ type: 'keyword.operator.label-matcher', value: '=' },
|
||||
{
|
||||
type: 'string.quoted.label-matcher',
|
||||
value: '"n"',
|
||||
index: 4,
|
||||
start: 13,
|
||||
},
|
||||
{ type: 'paren.rparen.label-matcher', value: '}' },
|
||||
],
|
||||
line: 'node_cpu{job="n"}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s: any, res: any) => {
|
||||
expect(res[0].meta).toEqual('label value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside by', () => {
|
||||
it('Should return label name list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {
|
||||
type: 'entity.name.tag.label-list-matcher',
|
||||
value: 'm',
|
||||
index: 9,
|
||||
start: 22,
|
||||
},
|
||||
tokens: [
|
||||
{ type: 'paren.lparen', value: '(' },
|
||||
{ type: 'keyword', value: 'count' },
|
||||
{ type: 'paren.lparen', value: '(' },
|
||||
{ type: 'identifier', value: 'node_cpu' },
|
||||
{ type: 'paren.rparen', value: '))' },
|
||||
{ type: 'text', value: ' ' },
|
||||
{ type: 'keyword.control', value: 'by' },
|
||||
{ type: 'text', value: ' ' },
|
||||
{ type: 'paren.lparen.label-list-matcher', value: '(' },
|
||||
{
|
||||
type: 'entity.name.tag.label-list-matcher',
|
||||
value: 'm',
|
||||
index: 9,
|
||||
start: 22,
|
||||
},
|
||||
{ type: 'paren.rparen.label-list-matcher', value: ')' },
|
||||
],
|
||||
line: '(count(node_cpu)) by (m)',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'm', (s: any, res: any) => {
|
||||
expect(res[0].meta).toEqual('label name');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -80,12 +80,13 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
items: [
|
||||
{ label: '1m' },
|
||||
{ label: '5m' },
|
||||
{ label: '10m' },
|
||||
{ label: '30m' },
|
||||
{ label: '1h' },
|
||||
{ label: '1d' },
|
||||
{ label: '$__interval', sortText: '$__interval' }, // TODO: figure out why this row and sortText is needed
|
||||
{ label: '1m', sortText: '00:01:00' },
|
||||
{ label: '5m', sortText: '00:05:00' },
|
||||
{ label: '10m', sortText: '00:10:00' },
|
||||
{ label: '30m', sortText: '00:30:00' },
|
||||
{ label: '1h', sortText: '01:00:00' },
|
||||
{ label: '1d', sortText: '24:00:00' },
|
||||
],
|
||||
label: 'Range vector',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user