Commit 4ddeb94f by David Committed by Hugo Häggmark

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
parent dda8b731
......@@ -69,7 +69,7 @@ export class KeybindingSrv {
}
exit() {
const popups = $('.popover.in');
const popups = $('.popover.in, .slate-typeahead');
if (popups.length > 0) {
return;
}
......
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>
);
}
}
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();
});
});
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 });
};
......
// 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>
`;
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,
......
<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' },
......
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 };
// 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";
});
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',
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment