diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index f47e2473a85..ffcbbb5fe64 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -7,15 +7,33 @@ export interface DataQueryResponse { } export interface DataQuery { + /** + * A - Z + */ refId: string; - [key: string]: any; + + /** + * true if query is disabled (ie not executed / sent to TSDB) + */ + hide?: boolean; + + /** + * Unique, guid like, string used in explore mode + */ + key?: string; + + /** + * For mixed data sources the selected datasource is on the query level. + * For non mixed scenarios this is undefined. + */ + datasource?: string | null; } -export interface DataQueryOptions { +export interface DataQueryOptions { timezone: string; range: TimeRange; rangeRaw: RawTimeRange; - targets: DataQuery[]; + targets: TQuery[]; panelId: number; dashboardId: number; cacheTimeout?: string; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 30a958420de..420a54e5840 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -2,11 +2,7 @@ import { ComponentClass } from 'react'; import { PanelProps, PanelOptionsProps } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource'; -export interface DataSourceApi { - name: string; - meta: PluginMeta; - pluginExports: PluginExports; - +export interface DataSourceApi { /** * min interval range */ @@ -15,7 +11,7 @@ export interface DataSourceApi { /** * Imports queries from a different datasource */ - importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise; + importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise; /** * Initializes a datasource after instantiation @@ -25,7 +21,7 @@ export interface DataSourceApi { /** * Main metrics / data query action */ - query(options: DataQueryOptions): Promise; + query(options: DataQueryOptions): Promise; /** * Test & verify datasource settings & connection details @@ -35,20 +31,27 @@ export interface DataSourceApi { /** * Get hints for query improvements */ - getQueryHints(query: DataQuery, results: any[], ...rest: any): QueryHint[]; + getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[]; + + /** + * Set after constructor is called by Grafana + */ + name?: string; + meta?: PluginMeta; + pluginExports?: PluginExports; } -export interface QueryEditorProps { - datasource: DataSourceApi; - query: DataQuery; +export interface QueryEditorProps { + datasource: DSType; + query: TQuery; onExecuteQuery?: () => void; - onQueryChange?: (value: DataQuery) => void; + onQueryChange?: (value: TQuery) => void; } export interface PluginExports { - Datasource?: any; + Datasource?: DataSourceApi; QueryCtrl?: any; - QueryEditor?: ComponentClass; + QueryEditor?: ComponentClass>; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2d1c0d2ad71..45b70672bc6 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -203,7 +203,7 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { /** * A target is non-empty when it has keys (with non-empty values) other than refId and key. */ -export function hasNonEmptyQuery(queries: DataQuery[]): boolean { +export function hasNonEmptyQuery(queries: TQuery[]): boolean { return ( queries && queries.some( @@ -280,7 +280,11 @@ export function makeTimeSeriesList(dataList) { /** * Update the query history. Side-effect: store history in local storage */ -export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] { +export function updateHistory( + history: Array>, + datasourceId: string, + queries: T[] +): Array> { const ts = Date.now(); queries.forEach(query => { history = [{ query, ts }, ...history]; diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index b54cabc9012..b7e8a68c722 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -243,8 +243,6 @@ export class PanelModel { addQuery(query?: Partial) { query = query || { refId: 'A' }; query.refId = this.getNextQueryLetter(); - query.isNew = true; - this.targets.push(query); } diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 98c8a5f6da9..febb322acca 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -1,16 +1,21 @@ +// Libraries import React from 'react'; import Cascader from 'rc-cascader'; import PluginPrism from 'slate-prism'; import Prism from 'prismjs'; -import { DataQuery } from '@grafana/ui/src/types'; -import { TypeaheadOutput } from 'app/types/explore'; +// Components +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +// Utils & Services // dom also includes Element polyfills import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; + +// Types +import { LokiQuery } from '../types'; +import { TypeaheadOutput } from 'app/types/explore'; const PRISM_SYNTAX = 'promql'; @@ -63,10 +68,10 @@ interface LokiQueryFieldProps { error?: string | JSX.Element; hint?: any; history?: any[]; - initialQuery?: DataQuery; + initialQuery?: LokiQuery; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: LokiQuery, override?: boolean) => void; } interface LokiQueryFieldState { diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index b7f67ffc0e7..195ac194dad 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,4 +1,6 @@ import LokiDatasource from './datasource'; +import { LokiQuery } from './types'; +import { getQueryOptions } from 'test/helpers/getQueryOptions'; describe('LokiDatasource', () => { const instanceSettings: any = { @@ -13,12 +15,13 @@ describe('LokiDatasource', () => { replace: a => a, }; - const range = { from: 'now-6h', to: 'now' }; - test('should use default max lines when no limit given', () => { const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo' }] }); + const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); + + ds.query(options); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000'); }); @@ -28,7 +31,10 @@ describe('LokiDatasource', () => { const customSettings = { ...instanceSettings, jsonData: customData }; const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock); backendSrvMock.datasourceRequest = jest.fn(); - ds.query({ range, targets: [{ expr: 'foo' }] }); + + const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); + ds.query(options); + expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20'); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 893ce649613..95d7a2a830e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1,13 +1,18 @@ +// Libraries import _ from 'lodash'; +// Services & Utils import * as dateMath from 'app/core/utils/datemath'; -import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; -import { PluginMeta, DataQuery } from '@grafana/ui/src/types'; import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query'; - import LanguageProvider from './language_provider'; import { mergeStreamsToLogs } from './result_transformer'; import { formatQuery, parseQuery } from './query_utils'; +import { makeSeriesForLogs } from 'app/core/logs_model'; + +// Types +import { LogsStream, LogsModel } from 'app/core/logs_model'; +import { PluginMeta, DataQueryOptions } from '@grafana/ui/src/types'; +import { LokiQuery } from './types'; export const DEFAULT_MAX_LINES = 1000; @@ -68,7 +73,7 @@ export default class LokiDatasource { }; } - query(options): Promise<{ data: LogsStream[] }> { + query(options: DataQueryOptions): Promise<{ data: LogsStream[] }> { const queryTargets = options.targets .filter(target => target.expr) .map(target => this.prepareQueryTarget(target, options)); @@ -96,7 +101,7 @@ export default class LokiDatasource { }); } - async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { + async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise { return this.languageProvider.importQueries(queries, originMeta.id); } @@ -109,7 +114,7 @@ export default class LokiDatasource { }); } - modifyQuery(query: DataQuery, action: any): DataQuery { + modifyQuery(query: LokiQuery, action: any): LokiQuery { const parsed = parseQuery(query.expr || ''); let selector = parsed.query; switch (action.type) { @@ -124,7 +129,7 @@ export default class LokiDatasource { return { ...query, expr: expression }; } - getHighlighterExpression(query: DataQuery): string { + getHighlighterExpression(query: LokiQuery): string { return parseQuery(query.expr).regexp; } diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index 3532e81ccd9..115a0a5f11f 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -1,6 +1,12 @@ +// Libraries import _ from 'lodash'; import moment from 'moment'; +// Services & Utils +import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; +import syntax from './syntax'; + +// Types import { CompletionItem, CompletionItemGroup, @@ -9,9 +15,7 @@ import { TypeaheadOutput, HistoryItem, } from 'app/types/explore'; -import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; -import syntax from './syntax'; -import { DataQuery } from '@grafana/ui/src/types'; +import { LokiQuery } from './types'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -20,7 +24,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const wrapLabel = (label: string) => ({ label }); -export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem { +type LokiHistoryItem = HistoryItem; + +export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem { const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label); const count = historyForItem.length; @@ -155,7 +161,7 @@ export default class LokiLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } - async importQueries(queries: DataQuery[], datasourceType: string): Promise { + async importQueries(queries: LokiQuery[], datasourceType: string): Promise { if (datasourceType === 'prometheus') { return Promise.all( queries.map(async query => { diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts new file mode 100644 index 00000000000..7325239bb3a --- /dev/null +++ b/public/app/plugins/datasource/loki/types.ts @@ -0,0 +1,6 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface LokiQuery extends DataQuery { + expr: string; +} + diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 391d39836ca..6fd450394a3 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -11,7 +11,7 @@ import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/ import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; -import { DataQuery } from '@grafana/ui/src/types'; +import { PromQuery } from '../types'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -88,13 +88,13 @@ interface CascaderOption { interface PromQueryFieldProps { datasource: any; error?: string | JSX.Element; - initialQuery: DataQuery; + initialQuery: PromQuery; hint?: any; history?: any[]; metricsByPrefix?: CascaderOption[]; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: PromQuery, override?: boolean) => void; } interface PromQueryFieldState { @@ -166,7 +166,7 @@ class PromQueryField extends React.PureComponent - group.rules.filter(rule => rule.type === 'recording').reduce( - (acc, rule) => ({ - ...acc, - [rule.name]: rule.query, - }), - mapping - ), - {} - ); -} - -export function prometheusRegularEscape(value) { - if (typeof value === 'string') { - return value.replace(/'/g, "\\\\'"); - } - return value; -} - -export function prometheusSpecialRegexEscape(value) { - if (typeof value === 'string') { - return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&')); - } - return value; -} - -export class PrometheusDatasource { +export class PrometheusDatasource implements DataSourceApi { type: string; editorSrc: string; name: string; @@ -149,7 +116,7 @@ export class PrometheusDatasource { return this.templateSrv.variableExists(target.expr); } - query(options) { + query(options: DataQueryOptions) { const start = this.getPrometheusTime(options.range.from, false); const end = this.getPrometheusTime(options.range.to, true); @@ -423,7 +390,7 @@ export class PrometheusDatasource { }); } - getExploreState(queries: DataQuery[]): Partial { + getExploreState(queries: PromQuery[]): Partial { let state: Partial = { datasource: this.name }; if (queries && queries.length > 0) { const expandedQueries = queries.map(query => ({ @@ -438,7 +405,7 @@ export class PrometheusDatasource { return state; } - getQueryHints(query: DataQuery, result: any[]) { + getQueryHints(query: PromQuery, result: any[]) { return getQueryHints(query.expr || '', result, this); } @@ -457,7 +424,7 @@ export class PrometheusDatasource { }); } - modifyQuery(query: DataQuery, action: any): DataQuery { + modifyQuery(query: PromQuery, action: any): PromQuery { let expression = query.expr || ''; switch (action.type) { case 'ADD_FILTER': { @@ -507,3 +474,40 @@ export class PrometheusDatasource { return this.resultTransformer.getOriginalMetricName(labelData); } } + +export function alignRange(start, end, step) { + const alignedEnd = Math.ceil(end / step) * step; + const alignedStart = Math.floor(start / step) * step; + return { + end: alignedEnd, + start: alignedStart, + }; +} + +export function extractRuleMappingFromGroups(groups: any[]) { + return groups.reduce( + (mapping, group) => + group.rules.filter(rule => rule.type === 'recording').reduce( + (acc, rule) => ({ + ...acc, + [rule.name]: rule.query, + }), + mapping + ), + {} + ); +} + +export function prometheusRegularEscape(value) { + if (typeof value === 'string') { + return value.replace(/'/g, "\\\\'"); + } + return value; +} + +export function prometheusSpecialRegexEscape(value) { + if (typeof value === 'string') { + return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&')); + } + return value; +} diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts new file mode 100644 index 00000000000..5bdc687d774 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -0,0 +1,6 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface PromQuery extends DataQuery { + expr: string; +} + diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index 25a811da42d..20b86d571df 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -10,18 +10,17 @@ import { FormLabel, Select, SelectOptionItem } from '@grafana/ui'; // Types import { QueryEditorProps } from '@grafana/ui/src/types'; - -interface Scenario { - id: string; - name: string; -} +import { TestDataDatasource } from './datasource'; +import { TestDataQuery, Scenario } from './types'; interface State { scenarioList: Scenario[]; current: Scenario | null; } -export class QueryEditor extends PureComponent { +type Props = QueryEditorProps; + +export class QueryEditor extends PureComponent { backendSrv: BackendSrv = getBackendSrv(); state: State = { @@ -30,11 +29,12 @@ export class QueryEditor extends PureComponent { }; async componentDidMount() { - const { query } = this.props; + const { query, datasource } = this.props; query.scenarioId = query.scenarioId || 'random_walk'; - const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios'); + // const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios'); + const scenarioList = await datasource.getScenarios(); const current = _.find(scenarioList, { id: query.scenarioId }); this.setState({ scenarioList: scenarioList, current: current }); diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 989209792fb..6ba0da59457 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -1,15 +1,17 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; +import { DataSourceApi, DataQueryOptions } from '@grafana/ui'; +import { TestDataQuery, Scenario } from './types'; -class TestDataDatasource { - id: any; +export class TestDataDatasource implements DataSourceApi { + id: number; /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q) { this.id = instanceSettings.id; } - query(options) { + query(options: DataQueryOptions) { const queries = _.filter(options.targets, item => { return item.hide !== true; }).map(item => { @@ -91,6 +93,9 @@ class TestDataDatasource { message: 'Data source is working', }); } + + getScenarios(): Promise { + return this.backendSrv.get('/api/tsdb/testdata/scenarios'); + } } -export { TestDataDatasource }; diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts new file mode 100644 index 00000000000..e233c3ef7a0 --- /dev/null +++ b/public/app/plugins/datasource/testdata/types.ts @@ -0,0 +1,11 @@ +import { DataQuery } from '@grafana/ui/src/types'; + +export interface TestDataQuery extends DataQuery { + scenarioId: string; +} + +export interface Scenario { + id: string; + name: string; +} + diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index e5ae676ba25..c69e93ff88e 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -243,9 +243,9 @@ export interface ExploreUrlState { range: RawTimeRange; } -export interface HistoryItem { +export interface HistoryItem { ts: number; - query: DataQuery; + query: TQuery; } export abstract class LanguageProvider { diff --git a/public/test/helpers/getQueryOptions.ts b/public/test/helpers/getQueryOptions.ts new file mode 100644 index 00000000000..ac92c2afb55 --- /dev/null +++ b/public/test/helpers/getQueryOptions.ts @@ -0,0 +1,25 @@ +import { DataQueryOptions, DataQuery } from '@grafana/ui'; +import moment from 'moment'; + + +export function getQueryOptions(options: Partial>): DataQueryOptions { + const raw = {from: 'now', to: 'now-1h'}; + const range = { from: moment(), to: moment(), raw: raw}; + + const defaults: DataQueryOptions = { + range: range, + rangeRaw: raw, + targets: [], + scopedVars: {}, + timezone: 'browser', + panelId: 1, + dashboardId: 1, + interval: '60s', + intervalMs: 60000, + maxDataPoints: 500, + }; + + Object.assign(defaults, options); + + return defaults; +}